Contratos do integrador¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
O que o integrador combina com quem o usa, nas duas direções: a ingestão (como um sistema entrega dados ao barramento — o ERP X-Adm e os verticais, ex. a integração PIED) e o retorno (como o vertical do fluxo de entrada descobre o que o X-Adm fez com o que ele mandou). Mais o heartbeat do integrador-client, que passa por aqui a caminho do central-backend.
Esta página é a fonte desses contratos: cada seção abaixo é um arquivo-fato deste repo, publicado também cru para que outro repositório o importe no build em vez de copiá-lo. Quem integra deve linkar para cá ou importar o cru — re-digitar o contrato do outro lado é o que produz o exemplo que mente: a cópia nasce certa e envelhece calada, sem nada apontando que ela divergiu.
| Fato | Cru, para importar |
|---|---|
| Envelope, autenticação, resposta e origens | https://docs.xadm.biz/aplicacoes/integrador-server/raw/xadm-ingest.md |
Tabela ESTOQUE |
https://docs.xadm.biz/aplicacoes/integrador-server/raw/xadm-ingest-estoque.md |
| Retorno do X-Adm (leitura + re-armar) | https://docs.xadm.biz/aplicacoes/integrador-server/raw/xadm-retorno.md |
| Manifesto de remessa (consulta + fechar à mão) | https://docs.xadm.biz/aplicacoes/integrador-server/raw/integracao-remessa.md |
| Heartbeat do integrador-client | https://docs.xadm.biz/aplicacoes/integrador-server/raw/heartbeat.md |
A forma completa de cada rota — parâmetros, schemas, exemplos — é o OpenAPI gerado do código.
Envelope, autenticação e resposta¶
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).
Manifesto de remessa — a unidade de entrega ao ERP¶
O ingest não grava só as linhas: ele declara quais precisam chegar juntas ao ERP. Isso nasce na mesma transação do apply, replica pelo PowerSync junto com as próprias linhas, e é o que permite ao coletor perguntar "tenho tudo desta remessa?" em vez de deduzir dependência entre tabelas.
Por que existe. O ERP rejeita filho sem pai, em silêncio e de forma terminal (1XX), e o
coletor lê só 000/9XX — a linha rejeitada fica invisível para sempre. O ingest é atômico no
Postgres, mas essa atomicidade se perde na fronteira do PowerSync: linhas da mesma transação
chegam ao cliente em checkpoints diferentes. Medido em produção, três formatos do mesmo defeito:
| pai ausente | filho rejeitado | mensagem do ERP |
|---|---|---|
propriedades (cliente) |
contrato → item → fórmulas | 120-Cliente CPF … não cadastrado. |
contratos (pedido) |
item, fórmulas | 120-Pedido (…) não cadastrado. |
estoque (produto) |
item, fórmulas | 120-Produto para o pedido (…) não cadastrado. |
As duas tabelas.
| tabela | o que é |
|---|---|
integracao_remessa |
a unidade: origem, chave_negocio, status, total_itens |
integracao_remessa_item |
as linhas dela: tabela, linha_id, bloqueado, resolvido |
Quando nasce. Uma remessa por grupo com dependência (duas ou mais linhas ligadas) — não por
push. Linha solta (propriedades avulsa, estoque avulso) não gera remessa e segue coletável
como antes: o manifesto acrescenta garantia, nunca remove. chave_negocio é o xPed no grupo de
pedido e o cgc_cpf no grupo de cliente. Entra na remessa só o que ainda precisa chegar ao ERP
(000, 9XX ou nulo); 006 e 1XX ficam de fora.
Uma remessa VIVA por chave (ABERTA ou TRAVADA) — invariante de banco. As ENTREGUE e
RESOLVIDA_MANUAL acumulam como histórico. Um re-push reusa a viva, inclusive travada; é o que faz o
resgate fechar.
Os estados, derivados no servidor a partir dos cod_retorno — exceto o último, que nasce de
comando:
status |
significa |
|---|---|
ABERTA |
ainda não entregue, sem rejeição terminal |
ENTREGUE |
todas as linhas aceitas (006) — vira histórico |
TRAVADA |
alguma linha em 1XX; só o re-arme a reabre |
RESOLVIDA_MANUAL |
o operador lançou o pedido à mão no X-Adm e fechou a remessa por comando; terminal |
As duas flags por item, e por que existem:
bloqueado— um pai desta linha, na mesma remessa, está1XX. Enviá-la repetiria o dano (uma ausência virou nove linhas terminais no incidente). O consumidor lê a flag; não deduz dependência. É também o que distingue rejeição real de colateral: quem reporta ao operador deve nomear a mensagem do item não bloqueado.resolvido— nada mais a esperar por esta linha:006, soft-deletada ou órfã. Existe porque, do lado do cliente, linha deletada e linha que ainda não chegou são o mesmo estado observável (a colunadeletednão sincroniza e a linha some do banco local). Sem a flag, a remessa ficaria retida para sempre lá.
total_itens não é cache de COUNT(*). É o que deixa o consumidor detectar lista de itens
rasgada: ele conta o que recebeu e compara com o declarado. Sem isso concluiria "tenho tudo" sobre
uma lista truncada e liberaria cedo — o mesmo modo de falha do incidente, um nível acima.
Tudo o que é derivado sai de uma função só, chamada de quatro lugares: o apply (PUT), o
soft-delete (DELETE), o write-back do PowerSync e o re-arme. Uma fonte só é o que impede o drift. A
única exceção é RESOLVIDA_MANUAL, que a função não escreve nem reabre.
Consulta: GET /api/v1/integracao/remessas?origem=&chave=&status= — chave aceita repetição.
Nas TRAVADA vêm também os itens em 1XX com codRetorno, msgRetorno e bloqueado. A resposta
completa e o fechamento à mão (POST …/remessas/resolver-manual) estão no fato próprio,
integracao-remessa.md.
Re-arme: POST /api/v1/xadm/retorno/pedido/reenfileirar?chave= re-arma todos os itens 1XX
da remessa viva — qualquer que seja a tabela, inclusive o pai. Sem isso, remessa cujo 1XX é um
pai não sairia de TRAVADA por caminho nenhum. xPed segue valendo como alias.
A tabela ESTOQUE¶
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.
Retorno do X-Adm: leitura e re-armar¶
Endpoint de leitura e autenticação¶
GET /api/v1/xadm/retorno/{tabela}?<chave natural>
Fecha o loop do fluxo de entrada: quem escreveu por PUT /api/v1/xadm pergunta aqui o
que o X-Adm fez com aquela linha. As colunas de retorno (CodRetorno, MsgRetorno,
Chave*) são do X-Adm — ele as grava pelo write-back, e este endpoint as lê.
{tabela} é case-insensitive e aceita cinco valores — as tabelas do fluxo de
entrada, não as 14 da ingestão:
{tabela} |
Chave natural obrigatória |
|---|---|
contratos |
xPed |
estoque |
codigoAlt |
itensped |
xPed e codigoAlt |
propriedades |
cgcCpf |
fones |
cgcCpf |
A chave é a mesma que se enviou na ingestão: o code do PIED (~9 chars), nunca o
id/ObjectId. Chave de outra tabela vai junto? É ignorada — só a obrigatória daquela
tabela conta, e se ela faltar (ou vier branca) a resposta é 400.
?origem= é aceito e ignorado: não filtra nada (a chave natural já é única). Existe
por conveniência de quem chama.
Toda rota /api/** exige Authorization: Bearer <token> — o mesmo token estático por
instalação da ingestão. Sem ele, 401.
O GET é read-only: não grava auditoria em request (é poll de alta frequência —
divergência consciente do GET /api/health, que audita). A única escrita deste contrato é o
POST .../pedido/reenfileirar, mais abaixo.
Resposta da leitura¶
Linha ausente não é 404. Consulta válida responde 200 sempre — inclusive
quando não há linha alguma. Quem consome decide pelo campo status, não pelo status HTTP:
poll que trate 404 como "ainda não chegou" nunca vai vê-lo.
{ "tabela": "fones", "status": "CONFIRMADO", "codRetorno": "006", "chaveXadm": "FONE-1" }
| Campo | Valor |
|---|---|
tabela |
a tabela consultada, em minúsculo (sempre presente) |
status |
NAO_ENCONTRADO, PENDENTE, CONFIRMADO ou REJEITADO (sempre presente) |
codRetorno |
o CodRetorno cru do X-Adm, sem o padding do CHAR |
msgRetorno |
a MsgRetorno crua do X-Adm |
chaveXadm |
a Chave* que o X-Adm gerou (ChaveCP/ChaveEst/ChaveItem/ChaveProp/ChaveFone) |
Os três campos crus somem do corpo quando não têm valor — não vêm como null. No
exemplo acima não há msgRetorno porque o X-Adm não gravou mensagem; em PENDENTE e
NAO_ENCONTRADO os três somem juntos. Cliente que exija os cinco campos quebra no caso
mais comum do poll.
| HTTP | Quando |
|---|---|
200 |
consulta válida — inclusive sem linha (NAO_ENCONTRADO) ou sem resposta do X-Adm (PENDENTE) |
400 |
{tabela} fora das cinco, ou chave natural obrigatória daquela tabela ausente/branca |
401 |
Bearer ausente ou inválido |
Como o status é derivado¶
O status é do integrador (derivado); o codRetorno é do X-Adm (cru). Os dois voltam
porque o derivado perde informação — veja REJEITADO.
status |
Quando | O que fazer |
|---|---|---|
NAO_ENCONTRADO |
nenhuma linha viva com essa chave natural | reenviar a linha |
PENDENTE |
a linha existe, mas o X-Adm ainda não respondeu — codRetorno branco ou 000/00X (≠006) |
esperar e repolar |
CONFIRMADO |
codRetorno = 006 — gravado no X-Adm |
reconciliar e parar de polar |
REJEITADO |
codRetorno não começa com 0 — 1XX (erro terminal) ou 9XX (erro de processamento) |
olhar o codRetorno cru: 1XX não adianta reenviar; 9XX é reprocessável |
NAO_ENCONTRADO é o único status ambíguo: linha soft-deleted responde igual a linha
que nunca chegou. Quem apagou sabe que apagou — mas não espere o endpoint distinguir.
Os códigos são do X-Adm (doc-mãe MAXSUL2025_610110), não deste repo: um código novo que
não comece com 0 cai em REJEITADO por padrão, e é o codRetorno cru que conta a
história.
Re-armar as linhas 1XX de um pedido¶
POST /api/v1/xadm/retorno/pedido/reenfileirar?xPed=<code do pedido no PIED>
A operação simétrica da leitura, e a única escrita deste contrato: devolve a 000 (e
apaga a MsgRetorno) as linhas do pedido que morreram em erro terminal — cod_retorno
começando com 1. É o que faz o integrador-client voltar a coletá-las.
Corpo vazio, comando na query string. A rota não lê corpo — aceita qualquer
Content-Type (inclusive nenhum, e text/plain, que é o que o int-pied manda). Não
responde 415.
Por que ela precisa existir. O upsert do PUT /api/v1/xadm preserva o retorno de
propósito (mão única): sem isso, todo re-push de rotina apagaria o retorno de um pedido já
gravado no ERP e o cliente o mandaria de novo — pedido duplicado. Consequência: reimportar
o pedido no painel do PIED nunca destrava um 1XX, por mais vezes que se clique. Este
endpoint é a exceção sob comando, para depois que o operador corrigiu o dado na origem.
Desde o manifesto de remessa, o re-arme é POR REMESSA, não por tabela. Ele alcança
todos os itens 1XX da remessa viva daquela chave — as seis tabelas do fluxo de entrada,
inclusive o pai (propriedades, fones, estoque).
Antes cobria só contratos/itensped/formulas, herança de quando a única chave era o
pied_x_ped. Como a remessa passou a declarar a cadeia inteira, isso virou um beco: remessa
cujo 1XX era um pai não saía de TRAVADA por caminho nenhum — o botão Reimportar
chamava um endpoint incapaz de resolver. Era o caso do 260079421, onde o pai que faltou foi
uma propriedades.
| parâmetro | efeito |
|---|---|
chave |
a chave da remessa: pied_x_ped do pedido ou cgc_cpf do cliente |
xPed |
alias histórico de chave — segue valendo |
Remessa de cliente (cadastro com contato, chaveada por cgc_cpf) também é re-armável por
aqui. Antes ela não tinha caminho de recuperação nenhum.
Pedido sem remessa viva — legado anterior ao backfill — continua re-armado pelo caminho
antigo: por pied_x_ped, nas três tabelas chaveadas por pedido. Some quando o backfill roda.
Linha de pedido resolvido à mão não volta. Mesmo com remessa viva, a linha de contratos/
itensped/formulas que também é item de uma remessa RESOLVIDA_MANUAL não é re-armada: o pedido
já foi lançado direto no X-Adm. O pai (propriedades/fones/estoque) segue re-armável — é
compartilhado entre pedidos.
Linha soft-deletada também não entra: a contagem é o que o operador lê no painel, e ela não pode incluir linha que, para ele, não existe mais.
O que NÃO é tocado — é o que torna a operação segura:
cod_retorno |
Por quê |
|---|---|
006 |
já gravado no ERP; re-armar faria o cliente reenviar cadastro que o ERP já tem — duplicaria |
9XX |
erro de processamento, já retenta sozinho |
000/00X |
já pendente; não há o que re-armar |
O efeito é uma transação só para as três tabelas: o PowerSync replica no commit, então o cliente nunca vê as três em estado intermediário.
Quem chama: o botão Reimportar do painel do int-pied, sempre depois do push do
pedido — a linha só volta a ser coletável já com o dado corrigido. O curl do runbook é o
mesmo comando na mão, para diagnóstico.
Uma ressalva honesta: um write-back do integrador-client em voo pode recarimbar 1XX logo
depois do re-armar. Janela estreita, e a operação é idempotente — o operador vê a contagem e
repete. Não há trava contra isso, de propósito: cobrir esse caminho serializaria o write-back
de todos os clientes.
Resposta do re-armar¶
{ "xPed": "260079421", "contratos": 1, "itensped": 3, "formulas": 2, "total": 6 }
| Campo | Valor |
|---|---|
xPed |
a chave recebida, sem espaços nas pontas |
contratos, itensped, formulas |
linhas re-armadas em cada tabela |
total |
a soma — o número que interessa ao operador |
| HTTP | Quando |
|---|---|
200 |
operação válida — inclusive pedido inexistente ou sem nada em erro (tudo zero) |
400 |
xPed ausente ou branco |
401 |
Bearer ausente ou inválido |
409 |
a remessa da chave foi resolvida à mão (RESOLVIDA_MANUAL) |
Pedido resolvido à mão não se re-arma. Se o operador lançou o pedido direto no X-Adm e fechou a
remessa pelo resolver-manual (ver o fato integracao-remessa.md), re-armar reenviaria ao ERP o que
já foi lançado. O pedido duplicaria. Por isso o 409, e não o fallback legado por pied_x_ped.
Zero não é erro. Pedido que não existe e pedido cujas linhas estão todas em 006
respondem igual: 200 com as contagens zeradas. Quem chama mostra a contagem ao operador —
total: 0 significa "não havia nada preso", não "falhou".
A operação é idempotente por natureza: chamar duas vezes seguidas devolve total: 0 na
segunda, porque a primeira já tirou as linhas do 1XX.
Manifesto de remessa: consulta e fechar à mão¶
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.
Heartbeat do integrador-client¶
Heartbeat: o integrador-client avisa que está vivo¶
POST /api/v1/heartbeat
Cada instalação do integrador-client manda um heartbeat no startup e a cada 60 min (versão, fluxos
ativos, hostname). Este integrador-server carimba o cliente_id da instância (env CLIENTE_ID) e
repassa ao central-backend em POST {CENTRAL_URL}/api/integrador/heartbeat, com o Bearer
CENTRAL_API_TOKEN. O que o central grava, o alerta de silêncio e a lista de instalações são do
central: contrato do repasse na
API REST do central-backend e o
porquê na decisão local 0028 do central.
- URL: esquema + host + porta de
powersync.upload.url, com o path/api/v1/heartbeat; ouheartbeat.urlquando definida. - Headers:
Authorization: Bearer <powersync.upload.token>(é oINTEGRADOR_API_TOKENda instância) eContent-Type: application/json.
Amostra canônica A:
{"versao":"0.2.0","fluxos":["pied"],"hostname":"SRV-ERP01","motivo":"inicio","iniciado_em":"2026-09-10T08:00:03-03:00"}
| Campo | Tipo no fio | Regra (validada pelo integrador-server) |
|---|---|---|
versao |
string | obrigatório, 1–40 caracteres |
fluxos |
array de string | sempre enviado pelo client; ≤ 20 itens, cada um ^[a-z0-9-]{1,50}$ (os nomes de Fluxo: pied, abastecimento, encerra-mdfe) |
hostname |
string ou null |
≤ 255; null quando o client não consegue resolver |
motivo |
string | inicio | periodico |
iniciado_em |
string | DateTimeFormatter.ISO_OFFSET_DATE_TIME sobre um OffsetDateTime truncado a segundos, no offset local da máquina; offset zero sai Z (ex. no CI em UTC). Sempre com segundos (o toString() do Java os omite quando são zero, por isso não serve). Não validado pelo integrador-server (repassa opaco) |
Respostas (corpo problem+json; o client só lê o status):
| Status | code |
Quando |
|---|---|---|
204 |
— | central aceitou |
400 |
BAD_REQUEST |
viola a tabela acima (versao, hostname, motivo, fluxos) |
401 |
— | Bearer ausente ou errado (ApiBearerSecurityRule) |
502 |
heartbeat_recusado |
central devolveu 4xx (inclusive corpo que não é problem+json) |
502 |
central_indisponivel |
central 5xx, timeout ou rede |
503 |
heartbeat_desligado |
CENTRAL_API_TOKEN ou CLIENTE_ID vazio no integrador-server |
Repasse sem transformação. versao, fluxos, hostname, motivo e iniciado_em seguem ao
central como vieram (iniciado_em é string opaca — quem a valida é o central); só entra o
cliente_id. Campo ausente vai como null, fluxos ausente vai como [], e campo que este contrato
não conhece é ignorado e não é repassado — é o que deixa o client ganhar campo sem quebrar o server.