Um SDK moldado pelo primeiro app que o usou
Vitor Ayres (abre em nova aba)
Publicamos o mercos-sdk, um cliente em TypeScript para a API do Mercos, em 19 de setembro de 2026. No dia seguinte, o primeiro app que o usou, um app interno de estoque de um distribuidor, entregou uma lista de dez achados, cada um marcado como medido ou inferência, com uma sugestão para o SDK. Seis viraram mudança no mesmo dia.
Este texto registra o que esse primeiro uso mudou no desenho do SDK, o que recusamos e por quê, e o que o app adotou ou não. O que a API do Mercos faz está no post sobre a API do Mercos. Aqui o assunto é o processo.
Resumo
- 10 versões em cerca de dois dias, da 0.1.0 à 0.4.3, com 34 commits.
- O app anotou 10 achados, todos medidos no sandbox, cada um com uma sugestão.
- A 0.4.0 respondeu a 6: gancho de observação, listagem por página, busca por id, criar e ler, intervalo mínimo, máscara de token e cliente congelado.
- Dissemos não a duas partes dos pedidos: espaçamento adaptativo, e leitura por id no sandbox com listagem em produção.
- O app não adotou tudo: criar e ler, e intervalo mínimo, ficaram de fora. A listagem por página só entrou 19 dias depois.
- No npm, publicação confiável com fila de staging. A
latestsegue a ordem de aprovação, não o semver. - A 0.4.2 e a 0.4.3 ficaram 19 dias na fila de staging antes de serem aprovadas.
O ponto de partida: uma requisição por vez
A 0.1.0 tinha o que a documentação do Mercos e os nossos testes no sandbox pediam: fetch nativo sem dependências, uma fila única de requisições por cliente, repetição automática no 429 com teto de espera, paginação por alterado_apos com o cursor recuado para não perder registros do mesmo segundo, até duas repetições de leitura em falha de rede e nunca de escrita, um tipo de erro que normaliza os quatro formatos do campo erros, e tipos para as 169 operações documentadas.
A fila existe porque o limite de requisições do Mercos é global. Disparar em paralelo só disputa o mesmo limite. No 429, o SDK espera o tempo que a resposta pede, mais meio segundo.
O primeiro commit é de 19 de setembro à 1h07.
Os dez achados do app
Em 20 de setembro, o app sincronizou seis cadastros e enviou um pedido no sandbox, com a 0.1.0 e depois com a 0.3.0, e anotou o que viu:
| # | Achado | Sugestão para o SDK |
|---|---|---|
| 1 | O Mercos soma o IPI do cadastro ao item do pedido | documentar a conta |
| 2 | O ST do cadastro não entra no pedido | documentar |
| 3 | “Sem valor” vem como 0 ou "", não como null |
normalizar ou documentar |
| 4 | O POST /v2/pedidos não devolve o total |
utilitário de leitura de volta |
| 5 | Ler pedido por id só existe no sandbox | utilitário que cubra os dois ambientes |
| 6 | Quase toda chamada leva um 429 antes do 200 | espaçar as requisições no cliente |
| 7 | A máscara de token altera os dados da resposta | mascarar só mensagens |
| 8 | Não há como observar as requisições sem embrulhar o fetch |
gancho de observação |
| 9 | O cliente aceita ter um recurso sobrescrito | congelar o objeto |
| 10 | Quantidade fracionada e item sem tabela são aceitos | nada, só registro |
Os achados 1, 2, 3 e 10 são comportamento da API, e ficaram como documentação. Dois dos outros merecem detalhe.
O achado 7 veio de um teste com o token "a". O SDK trocava o texto do token por *** em tudo que devolvia, e o campo total do pedido voltou como tot***l. Com tokens reais, longos e aleatórios, a colisão é improvável. Mesmo assim, a máscara reescrevia um JSON que a aplicação ia usar como dado.
O achado 9 foi erro do app, e por isso virou mudança no SDK. O app fez Object.assign sobre o cliente para embrulhar um recurso, o recurso novo chamava o antigo, e depois do assign a chamada apontava para ele mesmo. Recursão infinita, que só apareceu no envio real. Um cliente congelado falha na hora da escrita, com a causa ao lado.
O que entrou na 0.4.0, e o que mudou no caminho
A 0.4.0 saiu às 20h36 do mesmo dia e respondeu aos achados 4 a 9: createAndRead em pedidos, find em todo recurso, a opção minIntervalMs, a máscara só no que alimenta um erro, a opção onAttempt com listPages, e o cliente congelado.
Três pedidos mudaram de forma no caminho.
onRequest virou onAttempt. O app pediu um gancho onRequest ou onResponse. O nome ficou onAttempt porque um timeout não tem resposta, e o gancho dispara em toda tentativa, com método, rota modelo, status, duração, número da tentativa e a espera aplicada. O evento não carrega corpo, query nem cabeçalho, então um log dele não vaza token nem dado. E uma exceção dentro do gancho é engolida: um observador não pode quebrar uma requisição.
find lista sempre. O app sugeriu leitura por id no sandbox e listagem em produção, escondendo a diferença de ambiente. O SDK lista nos dois. Com um caminho só, o que o sandbox e a homologação exercitam é o que produção roda. O since é obrigatório, porque sem ele uma busca sem resultado percorreria o histórico inteiro da conta.
minIntervalMs não é adaptativo. O app sugeriu que o SDK aprendesse o intervalo a partir do retry-after do 429. Recusamos. O limite de produção pode ser outro, e um intervalo aprendido de um único 429 poderia deixar um cliente lento para sempre. Um número explícito é previsível. A medição no sandbox, seis leituras seguidas com cinco 429 contra zero com 5,5 segundos de intervalo, está no post sobre a API.
O que o app adotou, e o que não
| Mudança | O app adotou? |
|---|---|
find |
sim |
| máscara só em erros | sim, vem com a versão |
onAttempt |
sim, no lugar do embrulho do fetch |
| cliente congelado | sim, a porta virou um objeto novo em volta do cliente |
createAndRead |
não: se a leitura falhar, o id do pedido se perde |
minIntervalMs |
não: falta medir qual intervalo o Mercos aceita |
listPages |
só em 9 de outubro, com a 0.4.3 |
A recusa do createAndRead tinha razão, e virou a segunda rodada.
Segunda rodada: 0.4.1 a 0.4.3
Com o onAttempt ligado, o app mediu mais quatro coisas no mesmo dia. O sandbox pede 5,5 segundos depois de cada requisição, e quatro leituras seguidas viraram sete requisições. O onAttempt não dizia por que não houve resposta, se timeout ou falha de rede. O since do find exigia o formato e o fuso de ultima_alteracao, que o app montava à mão. E o createAndRead não dizia o que acontece quando a leitura falha depois de o pedido existir.
A 0.4.1, às 21h19, respondeu a três dos quatro: o erro de createAndRead passou a carregar o id criado em createdId, o since passou a aceitar um Date convertido para o horário do Brasil, e o evento de onAttempt ganhou um campo error com "network" ou "timeout". O espaçamento adaptativo continuou recusado.
A 0.4.2, às 23h38, veio de um teste nosso: o cursor das listagens passou a sair com espaço em vez de T, porque a rota de divisões respondia 422 ao formato da documentação e todas aceitam o espaço. A 0.4.3, às 23h50, corrigiu o tipo de alteração de pedido: um item sem id era incluído de novo em vez de alterado, e o tipo não tinha o campo.
A tabela inteira, com a hora do commit no horário de Brasília e a publicação no npm em UTC:
| Versão | Commit | npm | Origem |
|---|---|---|---|
| 0.1.0 | 19/09 18:56 | 19/09 22:03Z, à mão | primeira versão |
| 0.1.1 | 19/09 19:36 | nunca | campo vazio no erro de validação |
| 0.1.2 | 19/09 19:57 | 19/09 23:00Z | fila de staging |
| 0.2.0 | 20/09 13:11 | 20/09 21:28Z | estoque, categorias, formas de pagamento |
| 0.3.0 | 20/09 18:18 | 20/09 21:28Z | request, list e resource genéricos |
| 0.3.1 | 20/09 18:47 | 20/09 23:39Z | tipo do pedido e filtros |
| 0.4.0 | 20/09 20:36 | 20/09 23:41Z | seis achados do app |
| 0.4.1 | 20/09 21:19 | 21/09 01:09Z | segunda rodada |
| 0.4.2 | 20/09 23:38 | 09/10 19:33Z | cursor com espaço |
| 0.4.3 | 20/09 23:50 | 09/10 19:34Z, latest |
id no item da alteração |
Publicar no npm sem token
O pacote usa a publicação confiável do npm: nenhum token guardado, e o workflow de release prova a origem com OIDC. A primeira tentativa, a 0.1.1, recebeu 403 com “OIDC permission denied”. O publicador confiável tinha permissão só para mandar a versão para a fila de staging, não para publicar direto. A 0.1.1 ficou com tag e sem npm, e desde a 0.1.2 o fluxo é: a tag dispara o workflow, o workflow faz npm stage publish com atestado de procedência, e alguém aprova no site do npm, com 2FA. A publicação em staging existe desde maio de 2026. O staging exige npm 11.15, mais novo que o npm do Node 24.
Desde a 0.4.2, a release é um comando, pnpm release patch, que ajusta a versão, data o changelog, roda a verificação, commita, cria a tag assinada e envia branch e tag juntos.
A pegadinha é a latest. Ela vai para a última versão aprovada, sem olhar o semver. Em 20 de setembro, a 0.3.0 foi aprovada às 21:28:39 e a 0.2.0 seis segundos depois, então a latest ficou na 0.2.0 até a aprovação da 0.3.1, duas horas mais tarde. A regra ficou escrita no README: com duas versões na fila, aprovar na ordem das versões.
A fila também guarda o que ninguém aprova. A 0.4.2 e a 0.4.3 tiveram tag, Release no GitHub e workflow concluído em 20 de setembro, e ficaram em staging até 9 de outubro, quando foram aprovadas com 16 segundos de intervalo, na ordem certa. Durante 19 dias a latest foi a 0.4.1, e ninguém percebeu, porque o app que motivou tudo isso estava fixado na 0.4.0. No mesmo 9 de outubro ele passou para a 0.4.3 e adotou o listPages: o contador de requisições saiu, e com ele a requisição desperdiçada a cada corte de página.
Como verificamos
As horas dos commits vêm do git log do repositório, e as de publicação, do registro do npm, consultado em 9 de outubro de 2026, depois da aprovação das duas últimas versões. Os achados do app foram medidos no sandbox do Mercos em 20 de setembro, com o SDK 0.1.0, 0.3.0 e 0.4.0, e estão num documento do app com a marca de medido ou inferência em cada item.
O que não medimos: o limite de requisições em produção, o código de erro da leitura por id bloqueada em produção, e se o cursor com espaço teria aparecido no app.
O SDK tem licença MIT e não tem vínculo com a Mercos. O app é de um cliente e não é público; o que ele contornou para caber no plano grátis da Cloudflare está no post sobre as gambiarras.
Se a sua empresa precisa de um cliente para uma API que a documentação descreve pela metade, é esse o trabalho que fazemos em código aberto e em integrações.
Perguntas
- Quanto tempo levou da primeira versão do mercos-sdk à 0.4.3?
- Cerca de 47 horas. O primeiro commit é de 19 de setembro de 2026 à 1h07, e a release 0.4.3 é de 20 de setembro às 23h50. São 10 tags e 34 commits.
- Quantos achados do app viraram mudança no SDK?
- Dos 10 achados da primeira rodada, 6 entraram na 0.4.0. Da segunda rodada, 3 dos 4 entraram na 0.4.1. Os outros são comportamento da API e ficaram como documentação.
- Por que a versão 0.1.1 não está no npm?
- O publicador confiável do npm recebeu 403 ao publicar direto. Desde a 0.1.2, a versão vai para a fila de staging e alguém aprova no site do npm, com 2FA.
- A tag latest do npm segue o semver?
- Não. Ela segue a última versão aprovada na fila. Com duas versões na fila, aprovar na ordem errada deixa a latest numa versão mais antiga, como aconteceu com a 0.2.0 e a 0.3.0.
- O app usa a versão mais nova do SDK?
- Desde 9 de outubro de 2026, sim: ele passou da 0.4.0 para a 0.4.3 e trocou o contador de requisições pelo listPages que tinha pedido. Ficou 19 dias atrás da latest.