Pular para conteúdo

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 coluna deleted nã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; ou heartbeat.url quando definida.
  • Headers: Authorization: Bearer <powersync.upload.token> (é o INTEGRADOR_API_TOKEN da instância) e Content-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.