API do Mercos: o que testamos no sandbox
Vitor Ayres (abre em nova aba)
O Mercos tem uma API de integração para pedidos, clientes, produtos, estoque e tabelas de preço. A documentação é pública. Antes de liberar o acesso à produção, o Mercos exige na homologação que a integração trate o limite de requisições e a paginação.
Este texto registra o que encontramos ao integrar com ela. A maior parte foi medida no sandbox; onde não foi, o texto diz. O sandbox pode se comportar diferente da produção, e a API pode mudar depois disso.
Resumo
- A API não funciona no navegador: recusa o preflight de CORS, e os tokens dão acesso total à conta.
- O limite de requisições vale para todas as rotas juntas. No sandbox, uma requisição a cada 5,5 segundos evitou o erro 429.
- A paginação é por data de alteração, com resolução de um segundo, o que exige cuidado no corte das páginas.
- As datas vêm no horário de Brasília.
- A API não cria orçamento. O pedido nasce com o status Gerado.
- O Mercos soma o IPI do cadastro a cada item do pedido, mesmo que você não mande.
- Em produção, não dá para ler um pedido pelo ID, segundo a documentação do Mercos.
Só do servidor
A API recusa o preflight de CORS, então uma chamada feita pelo navegador falha. Mesmo que funcionasse, não seria uma boa ideia: os dois tokens, o da aplicação e o da empresa, dão acesso total à conta.
As chamadas precisam sair do seu servidor, que expõe ao navegador só aquilo de que a sua aplicação precisa.
Limite de requisições
O limite é global: vale para todas as rotas somadas, e não por rota. Ao passar dele, a API responde 429 e informa no campo tempo_ate_permitir_novamente quanto esperar.
A consequência prática é que disparar várias requisições em paralelo não adianta. Elas disputam o mesmo limite.
No sandbox, medimos:
| Rodada | Respostas 429 | Tempo total |
|---|---|---|
| Seis leituras seguidas, sem intervalo | 5 | 33 s |
| Seis leituras com 5,5 s entre elas | 0 | 28 s |
A rodada espaçada terminou antes e precisou de metade das requisições, porque não precisou repetir nada. O limite de uma conta de produção pode ser outro, então vale medir na sua.
Outro cuidado: uma escrita que não teve resposta não deve ser reenviada às cegas. O pedido pode ter sido criado mesmo sem a resposta chegar.
Paginação por data de alteração
As listagens são incrementais. O cursor é o parâmetro alterado_apos, e o cabeçalho MEUSPEDIDOS_LIMITOU_REGISTROS com valor 1 avisa que há mais páginas.
O problema é a resolução. O campo ultima_alteracao tem precisão de um segundo, e o corte de uma página pode cair no meio de um segundo. Se a próxima página começar exatamente depois do último registro recebido, os registros restantes daquele segundo podem ficar de fora.
O que fizemos:
- Usar como próximo cursor a penúltima
ultima_alteracaodistinta da página, relendo o último segundo inteiro. Se a página termina em10:00:01,10:00:02e10:00:02, o próximo cursor é10:00:01, e os registros de10:00:02vêm de novo. - Descartar os registros que voltam repetidos por causa desse recuo.
- Tratar como erro o caso em que o cabeçalho avisa que há mais páginas e a página inteira tem a mesma
ultima_alteracao, porque aí não há para onde recuar. Em pedidos, pedir mais registros por página costuma resolver.
Para sincronizar de forma incremental entre uma execução e outra, guarde a maior ultima_alteracao recebida e use esse valor na próxima, relendo o último segundo e descartando os repetidos.
Datas no horário de Brasília
O Mercos grava ultima_alteracao no horário de Brasília. Uma alteração feita às 00:12 UTC voltou marcada como 21:12 do dia anterior. Quem trabalha em UTC precisa converter antes de usar a data como cursor.
O formato também tem uma pegadinha. A documentação escreve alterado_apos como 2024-04-10T15:45:00. A maioria das rotas aceita, mas /v1/divisoes responde 422 e exige um espaço no lugar do T: 2024-04-10 15:45:00. Todas as rotas do sandbox aceitaram o espaço, então é o formato mais seguro.
Pedidos
A maior parte das surpresas está aqui.
- Não existe orçamento pela API. O pedido criado pela API nasce com o status Gerado (
"2"). As rotas/v2/pedidose/v1/pedidosrespondem 422 a um campostatus, na criação e na alteração. Orçamento, o status"1", só sai pelas telas do Mercos. - A condição de pagamento é obrigatória, como
condicao_pagamento_idou como texto livre emcondicao_pagamento. Sem nenhuma das duas, a resposta é 422, embora a documentação não marque nenhuma como obrigatória. - O ID do pedido criado vem no cabeçalho
MeusPedidosID. - Em produção, não dá para ler um pedido pelo ID. Este ponto não vem dos nossos testes, e sim da página da rota: a leitura por ID existe só no sandbox, e depois da homologação ela não é permitida em produção, por desempenho. A documentação não diz qual erro a API devolve, e nós não medimos. A saída é procurar o pedido na listagem, filtrando por data de alteração. Quem precisar mesmo da leitura por ID pode pedir a liberação ao suporte do Mercos, com uma justificativa técnica.
- A alteração é parcial. Um item com
idaltera aquele item. Um item semidé incluído, mesmo que o produto já esteja no pedido. Um item comexcluido: trueé excluído, mas continua vindo na leitura, marcado como excluído. Os itens que o corpo não cita ficam como estão.
O IPI que você não mandou
O Mercos soma ao pedido o IPI do cadastro do produto, mesmo quando o item não manda ipi. Um exemplo medido no sandbox:
| Item | Enviado | Subtotal no Mercos | Conta que fecha |
|---|---|---|---|
| IPI de 5%, percentual | 3 × 400, desconto de 10% | 1134 | 3 × 360 × 1,05 |
| IPI de R$ 12,50, valor fixo | 4 × 200, desconto de 10% | 770 | 4 × 180 + 4 × 12,50 |
| Sem IPI | 2 × 150 | 300 | 2 × 150 |
Duas conclusões da tabela:
- O IPI percentual incide depois do desconto.
- O IPI em valor fixo é por unidade, e o desconto não o reduz.
Além disso, o arredondamento para centavos acontece no subtotal de cada item.
Uma tela que soma só preço, quantidade e desconto vai mostrar um total menor que o real. Depois de criar o pedido, consulte-o de novo para ter o valor que o Mercos gravou.
Já a substituição tributária (st) do cadastro não entrou no pedido: os itens voltaram com st: 0. Não sabemos o motivo.
Estoque
Aqui medimos pouco. A conta de sandbox está com o controle de estoque desligado, e o Mercos recusa qualquer ajuste nessa condição com 422 e uma mensagem que diz isso. Foi o único comportamento de estoque que vimos acontecer.
As regras abaixo vêm da documentação, e o SDK as trata, mas não as confirmamos no sandbox:
- O ajuste define o saldo, não soma nem subtrai. Você envia o saldo novo.
- Cada requisição aceita no máximo 300 ajustes. Se um deles der erro, o lote inteiro é cancelado e nenhum saldo muda.
- O ajuste vale para produtos simples e para os filhos de uma grade. No produto agregador, o pai da grade, a documentação diz que a API devolve erro.
Outras coisas que vale saber
- Valores ausentes vêm como
0ou"", não comonull:tabela_preco_id: 0,transportadora_id: 0,observacoes: "". Um0num campo de ID quer dizer “sem valor”. - Os erros vêm em quatro formatos diferentes, conforme a rota.
- A data de um campo extra vai como
yyyy-mm-dd. A documentação mostrayyyy-dd-mm, que é erro de digitação: a API recusa e informa o formato certo. - Não há rota para os dados da própria empresa, como logotipo ou CNPJ.
- Quantidade fracionada, como 1,5, é aceita.
Como verificamos
Os testes rodaram contra o sandbox do Mercos. Parte veio de uma suíte automatizada, que cria um pedido e o cancela no fim. O resto veio de uma aplicação nossa que usa o SDK no dia a dia, também apontada para o sandbox.
A documentação do Mercos traz trechos de OpenAPI embutidos em cada página. Juntamos esses trechos em um único arquivo, com 169 operações, e registramos as correções num arquivo à parte, cada uma com o motivo. Dois defeitos se repetem nas páginas: um objeto declarado quando o exemplo mostra uma lista, ou o contrário, e a mesma rota documentada com corpos diferentes em páginas diferentes.
Um SDK que já trata isso
Publicamos o mercos-sdk, um SDK em TypeScript com licença MIT. Ele usa o fetch nativo, sem dependências, e roda em Node 22 ou mais recente, Deno, Bun e Cloudflare Workers.
Ele envia uma requisição por vez e espera o tempo indicado na resposta 429, faz a paginação com o recuo descrito acima, nunca reenvia uma escrita, converte um Date para o horário de Brasília e junta os quatro formatos de erro em um só. Também se recusa a iniciar no navegador, para os tokens não vazarem. Por padrão ele não espaça as requisições; o intervalo de 5,5 segundos se configura com a opção minIntervalMs.
npm install mercos-sdk
import { createMercos } from "mercos-sdk";
const mercos = createMercos({
applicationToken: process.env.MERCOS_APPLICATION_TOKEN!,
companyToken: process.env.MERCOS_COMPANY_TOKEN!,
environment: "sandbox",
});
for await (const cliente of mercos.clientes.list({ changedAfter: "2024-01-01 00:00:00" })) {
console.log(cliente.id, cliente.razao_social);
}
O projeto não tem vínculo com a Mercos. Como o primeiro app que o usou mudou o desenho do SDK está no post sobre o SDK e o seu primeiro app, e o que fizemos para criar orçamento, que a API não faz, está no post sobre o caminho híbrido. Fizemos o mesmo tipo de levantamento com a API da ANVISA.
Se a sua empresa precisa ligar o Mercos a um ERP, a uma planilha ou a outro sistema, esse é o trabalho que fazemos em integrações.
Perguntas
- Dá para chamar a API do Mercos direto do navegador?
- Não. A API recusa o preflight de CORS, e os dois tokens dão acesso total à conta. As chamadas precisam sair de um servidor.
- Qual é o limite de requisições da API do Mercos?
- O limite vale para todas as rotas juntas. No sandbox, ele aceitava mais ou menos uma requisição a cada 5,5 segundos. O limite de uma conta de produção pode ser outro.
- Dá para criar um orçamento pela API do Mercos?
- Pelos nossos testes, não. O pedido criado pela API nasce com o status Gerado, e as rotas de pedido recusam o campo status com erro 422. Orçamento só sai pelas telas do Mercos.
- Por que o total do pedido no Mercos é maior que a soma dos itens?
- Porque o Mercos soma o IPI do cadastro do produto a cada item, mesmo quando o item não manda IPI. Consulte o pedido de novo depois de criar.
- Como sincronizar só o que mudou?
- Guarde a maior ultima_alteracao recebida e passe esse valor em alterado_apos na próxima consulta, relendo o último segundo e descartando os repetidos. A data vem no horário de Brasília.