Pular para o conteúdo
MARTINSLOG
Referência de integração · v0

API Martins Log

Cotação, criação de envio, pagamento por carteira, rastreio e webhooks — o que uma loja precisa para transformar uma venda paga em uma etiqueta e um código de rastreio, sem intervenção humana.

URL base
app.martinslog.net
Autenticação
Bearer token
Formato
JSON · UTF-8
Limite
60 req/min por token

Vai integrar? Leve esta página inteira — ela sai em texto, pronta para colar em qualquer lugar.

O fluxo em 4 chamadas

Uma venda paga na sua loja vira um envio despachado em quatro chamadas, nesta ordem. Cada uma depende do resultado da anterior.

01 /calculator Preço e prazo. Devolve o id de cada serviço.
02 /cart Cria o envio com remetente, destinatário e produtos.
03 /checkout Debita a carteira e emite a etiqueta.
04 /order/info Lê o código de rastreio. Ou espere o webhook.

O preço nunca viaja no corpo da requisição. A cotação fica salva no servidor e o /cart a referencia pelo id — se você mandar um campo price, ele é descartado em silêncio. Isso é proposital: o valor cobrado não pode depender do que o cliente envia.

Exemplo completo

# 1. cotar
curl -X POST https://app.martinslog.net/api/v0/calculator \
  -H "Authorization: Bearer frete_live_..." \
  -H "Content-Type: application/json" \
  -d '{"cepOrigem":"12321-313","cepDestino":"80010-000","formato":"CAIXA",
       "pesoRealG":1000,"alturaCm":20,"larguraCm":15,"comprimentoCm":10}'

# devolve, entre outras: {"id":"clx123:svc-eco","price":"12.30", ...}

# 2. criar o envio com o id da opção escolhida
curl -X POST https://app.martinslog.net/api/v0/cart \
  -H "Authorization: Bearer frete_live_..." \
  -H "Content-Type: application/json" \
  -d '{"service":"clx123:svc-eco","remetente":{...},"destinatario":{...},
       "produtos":[{"nome":"Kit 4em1","quantidade":1,"valorUnitarioCentavos":9790}]}'

# devolve: {"id":"shp_abc","price":"12.30","label_fee":"1.00","charged":false,"status":"PENDING"}

# 3. pagar
curl -X POST https://app.martinslog.net/api/v0/checkout \
  -H "Authorization: Bearer frete_live_..." \
  -H "Content-Type: application/json" \
  -d '{"orders":["shp_abc"]}'

# 4. ler o rastreio
curl https://app.martinslog.net/api/v0/order/info/shp_abc \
  -H "Authorization: Bearer frete_live_..."

Autenticação

Toda chamada de /api/v0 exige o token no cabeçalho:

Authorization: Bearer frete_live_a1b2c3...

O token é criado no painel, em Integrações, e pertence a uma conta. A carteira que paga o frete é a da conta dona do token — uma loja, um token.

PrefixoAmbienteEfeito
frete_test_SandboxNão encosta na carteira
frete_live_ProduçãoDebita de verdade
Guarde na hora

O token aparece uma única vez, na tela de criação. Só o hash fica gravado — não há como recuperá-lo depois, apenas revogar e criar outro.

Token ausente, malformado, inválido ou revogado responde 401 TOKEN_INVALIDO. O prefixo é rótulo, não credencial: a entropia inteira está nos 32 bytes que vêm depois dele.

Sandbox e produção

Mesma URL, mesmos campos, mesmas respostas. Só o token muda. Não existe host separado de teste.

ComportamentoSandboxProdução
Débito na carteiraNenhum lançamento é criadoDebita ao pagar
Código de rastreioSANDBOX + 12 hexEC000000014BR
Página pública de rastreio Responde 200 e mostra erro Abre o rastreio
Webhook "sandbox": truesó em order.created e order.released Sem esse campo
Campo chargedSempre falsetrue após o pagamento
Não confie só no campo sandbox do webhook

Ele não vem em todos os eventos. Só order.created e order.released são marcados. Um order.generated, order.posted ou order.delivered de um envio de teste chega idêntico a um real — e quem filtra teste por esse campo registra a entrega de um pedido que nunca existiu.

Duas formas seguras: guarde o envio como teste quando o order.created chegar marcado, ou consulte GET /api/v0/order/info/:id, que traz sandbox sempre, em qualquer estado.

Limite de requisições

60 requisições por minuto, por token. A contagem é por token, não por IP — uma loja não derruba a cota de outra que esteja atrás do mesmo servidor.

A janela é fixa, não deslizante: ela abre na primeira requisição e vale um minuto inteiro. Estourar no segundo 5 bloqueia os 55 segundos restantes — a cota não vai se liberando aos poucos. Distribua as chamadas em vez de disparar tudo de uma vez.

Estourando, a resposta é 429 LIMITE_REQUISICOES_EXCEDIDO com o cabeçalho Retry-After: 60 e o tempo real que falta, em segundos, na mensagem. Respeite o cabeçalho em vez de repetir imediatamente.

Uma venda consome 3 ou 4 chamadas. O teto prático é de cerca de 15 vendas por minuto por loja. Para importar histórico, enfileire — não existe endpoint em lote.

Erros

Todo erro tem a mesma forma:

{
  "codigo": "SALDO_INSUFICIENTE",
  "mensagem": "Saldo insuficiente para pagar este envio."
}

Erro de validação acrescenta campos, com a lista de problemas por campo:

{
  "codigo": "CORPO_INVALIDO",
  "mensagem": "Dados de cotação inválidos.",
  "campos": { "cepDestino": ["CEP de destino inválido"] }
}

Programe contra o codigo, nunca contra a mensagem — o texto muda, o código não.

HTTPCódigoQuando aconteceO que fazer
400CORPO_INVALIDOCampo faltando ou fora do formatoCorrigir e reenviar
400CEP_INVALIDOCEP não existe na baseCorrigir o endereço
401TOKEN_INVALIDOToken ausente, inválido ou revogadoNão repetir; conferir a credencial
402SALDO_INSUFICIENTECarteira sem saldo no /checkoutRecarregar; o envio segue PENDING
403NAO_AUTORIZADOAção fora do alcance do tokenNão repetir
404ENVIO_NAO_ENCONTRADOId inexistente ou de outra contaNão repetir
404COTACAO_NAO_ENCONTRADAservice aponta para cotação que sumiuCotar de novo
422COTACAO_EXPIRADACotação velha demaisCotar de novo
422TRANSICAO_INVALIDAEx.: pagar um envio já pagoLer o status antes
422ROTA_NAO_ATENDIDASem serviço para o par de CEPsOferecer outra opção
429LIMITE_REQUISICOES_EXCEDIDOMais de 60 chamadas no minutoEsperar o Retry-After
500ERRO_INTERNOFalha inesperada nossaRepetir com espera crescente

404 em vez de 403 é decisão de segurança. Um id que existe em outra conta responde exatamente como um id que não existe — a diferença de código revelaria quais ids são reais.

Os três números

Confundi-los é o erro de integração mais caro. São valores diferentes, com donos diferentes.

CampoO que éQuem paga
price O frete — o transporte em si O comprador da sua loja, no checkout dela
label_fee O que a plataforma cobra por etiqueta gerada — hoje R$ 1,00 fixo Você, lojista, da sua carteira
charged Se a taxa saiu de fato da carteira

Todos os valores monetários são strings em reais com duas casas ("12.30"), nunca centavos e nunca número. Nos campos de entrada vale o contrário: valorUnitarioCentavos é inteiro em centavos. A assimetria é real; não deduza uma coisa da outra.

charged é lido do livro-caixa, não do status nem da flag de sandbox. Some label_fee apenas onde charged for true, ou você vai contabilizar dinheiro que nunca saiu.

A taxa é por etiqueta, não proporcional ao frete: uma encomenda de R$ 12,30 e outra de R$ 180,00 custam o mesmo R$ 1,00 a você. O price aparece na etiqueta e no rastreio; o label_fee aparece no seu extrato. Nunca cobre o label_fee do comprador — ele não é frete.

1 · Cotação

POST /api/v0/calculator 200 · lista

Corpo

CampoTipoRegra
cepOrigemstringobrig.00000-000 ou 00000000
cepDestinostringobrig.idem
formatoenumobrig.CAIXA · ROLO · ENVELOPE
pesoRealGinteiroobrig.gramas, > 0
alturaCminteiroobrig.centímetros, > 0
larguraCminteiroobrig.centímetros, > 0
comprimentoCminteiroobrig.centímetros, > 0

Medidas são inteiras. 20.5 é recusado com CORPO_INVALIDO — arredonde para cima antes de enviar.

Resposta 200

[
  {
    "id": "clx123abc:svc-economico",
    "name": "Econômico",
    "price": "12.30",
    "discount": "3.70",
    "delivery_time": 5,
    "company": { "name": "Martins Log" }
  }
]
  • id — no formato quoteId:servicoId. É o que vai no service do passo 2, inteiro e sem alterações.
  • delivery_time — prazo em dias corridos, e a contagem começa na emissão da etiqueta (o evento order.generated), não na postagem nem na compra. Sábado, domingo e feriado contam. Se a sua loja anuncia o prazo em dias úteis, converta antes de mostrar ao comprador — são números diferentes.
  • Serviços indisponíveis para a rota não aparecem. Lista vazia é resposta válida e significa "não atendemos esse trecho" — trate na sua tela, não como erro.

2 · Criar envio

POST /api/v0/cart 201 · objeto

Corpo

CampoTipoRegra
servicestringobrig.o id vindo da cotação
remetenteendereçoobrig.tabela abaixo
destinatarioendereçoobrig.tabela abaixo
produtoslistaobrig.ao menos um item

Endereço

CampoTipoRegra
nomestringobrig.não vazio
cepstringobrig.00000-000 ou 00000000
logradourostringobrig.não vazio
numerostringobrig.string, não número — "S/N" é válido
bairrostringobrig.não vazio
cidadestringobrig.não vazio
ufstringobrig.exatamente 2 letras
documentostringopc.CPF ou CNPJ
emailstringopc.e-mail válido ou string vazia
telefonestringopc.
complementostringopc.

email tem uma pegadinha: se o campo existir, precisa ser um e-mail válido ou exatamente "". Mandar null ou "—" derruba a requisição inteira. Se não tem e-mail, omita a chave.

Produto

CampoTipoRegra
nomestringobrig.não vazio
quantidadeinteiroobrig.> 0
valorUnitarioCentavosinteiroobrig.centavos, > 0 — 9790 = R$ 97,90

A soma dos produtos vira o valor declarado do envio, que é a base do seguro. Declarar menos que o real reduz a cobertura.

Resposta 201

{
  "id": "shp_9f2c...",
  "price": "12.30",
  "label_fee": "1.00",
  "charged": false,
  "status": "PENDING"
}

Guarde o id no seu pedido agora. Ele é a única chave que liga a sua venda ao envio, e é o que impede a duplicação descrita no checklist.

3 · Pagar

POST /api/v0/checkout 200 · objeto

Debita a carteira e, em produção, emite a etiqueta na sequência. Você não precisa chamar mais nada para obter o código de rastreio.

Corpo

{ "orders": ["shp_9f2c..."] }

Aceita vários ids, mas o processamento é sequencial e sem transação: se o terceiro falhar por saldo, os dois primeiros já foram pagos e continuam pagos. Mandar um id por chamada torna o tratamento de erro muito mais simples.

Resposta 200

{
  "success": true,
  "purchase": {
    "status": "approved",
    "orders": [ { "id": "shp_9f2c...", "status": "GENERATED" } ]
  }
}
Leia o status devolvido

O status de cada ordem é relido do banco depois do pagamento. Ele costuma vir GENERATED — a etiqueta já saiu —, mas pode vir RELEASED se a emissão ainda não concluiu. Não assuma nenhum dos dois: use o valor devolvido, ou espere o webhook order.generated.

Sem saldo, a resposta é 402 SALDO_INSUFICIENTE e o envio permanece PENDING — nada se perde. Basta recarregar e chamar de novo com o mesmo id.

4 · Consultar

GET /api/v0/order/info/{id} 200 · objeto
{
  "id": "shp_9f2c...",
  "status": "POSTED",
  "tracking": "EC000000014BR",
  "tracking_url": "/r/EC000000014BR",
  "price": "12.30",
  "label_fee": "1.00",
  "charged": true,
  "sandbox": false,
  "returned_at": null,               // preenchido = voltou ao remetente
  "created_at": "2026-09-01T10:00:00.000Z"
}
tracking_url é relativo

Vem como /r/EC000000014BR, sem domínio. Concatene com https://app.martinslog.net antes de mostrar ao cliente. Mandar o caminho cru num e-mail gera um link quebrado.

tracking e tracking_url são null enquanto o envio não tem código — na prática, antes de GENERATED. Datas são sempre ISO 8601 em UTC.

Ciclo de vida do envio

Sete estados. As transições são verificadas no servidor: tentar um salto inválido devolve 422 TRANSICAO_INVALIDA.

StatusSignificaPode ir para
PENDINGCriado, não pagoRELEASED · CANCELLED
RELEASEDPago, etiqueta em emissãoGENERATED · CANCELLED
GENERATEDEtiqueta emitida, já tem códigoPOSTED · CANCELLED
POSTEDColetado, em trânsitoDELIVERED · LOST
DELIVEREDEntregue— final
CANCELLEDCancelado— final
LOSTExtraviado— final

Cancelamento só é possível até GENERATED. Depois de POSTED a carga está na rua e não há mais o que cancelar.

Dois casos que o webhook não distingue

LOST não dispara evento nenhum. Extravio não é cancelamento, e mandar order.cancelled descreveria errado o que houve com a carga. Se o extravio importa para você, consulte o status.

Devolução ao remetente também chega como order.delivered. A carga foi entregue — de volta a quem despachou. Pelo webhook os dois casos são idênticos: confirme em GET /api/v0/order/info/:id, onde returned_at preenchido significa que o pacote voltou. Sem essa conferência, a loja marca como entregue ao comprador um pacote que está de volta no estoque dela.

Webhooks

A alternativa a ficar consultando /order/info. Nós avisamos a cada mudança de estado.

O cadastro não é pela API

POST /api/v0/webhook exige sessão do painel, não aceita o token de API. Quem cadastra a URL é o dono da loja, logado em Integrações. Não há como automatizar isso do seu lado.

Regras da URL de destino

  • https obrigatório — http é recusado.
  • Sem usuário e senha embutidos na URL.
  • Sem localhost, .localhost, .internal ou IP de rede privada.

URL recusada devolve 422 ARQUIVO_INVALIDO com o motivo.

Eventos

EventoDisparado quandotracking
order.createdO envio é criado (/cart)null
order.releasedO pagamento é confirmadonull
order.generatedA etiqueta sai — o código nasce aquipreenchido
order.postedA carga é coletadapreenchido
order.deliveredA entrega é concluídapreenchido
order.cancelledO envio é canceladovaria

Se você só puder escutar um, escute order.generated — é onde o código de rastreio aparece.

Corpo entregue

{
  "event": "order.generated",
  "data": {
    "id": "shp_9f2c...",
    "status": "GENERATED",
    "tracking": "EC000000014BR",
    "tracking_url": "/r/EC000000014BR",   // relativo
    "price": "12.30",                     // string, em reais
    "created_at": "2026-09-01T10:00:00.000Z"
  },
  "sent_at": "2026-09-01T10:00:01.000Z"
}

// `sandbox: true` entra no mesmo nível de `event` — mas SÓ em
// order.created e order.released. Este exemplo é order.generated,
// que nunca traz o campo, nem quando o envio é de teste.

Entrega e retentativa

ParâmetroValor
Atraso até a primeira tentativa até 1 minuto
Tempo limite por tentativa5 segundos
Tentativas totais6 (1 imediata + 5 reagendadas)
Intervalos1 min · 5 min · 30 min · 2 h · 12 h
Janela total≈ 14 horas
Repete emfalha de rede · 408 · 429 · 5xx
Desiste emqualquer outro 4xx

A fila é processada por um agendador que roda a cada minuto. Um evento não sai no mesmo instante em que acontece: espere até um minuto entre a mudança de status e a chegada da entrega. Não é fila travada.

Responda rápido e com 2xx. Grave o evento numa fila sua e processe depois; passar de 5 segundos conta como falha e gera reentrega. Um 404 (rota errada) faz o sistema desistir na primeira tentativa — o que é proposital: repetir contra uma URL inexistente por catorze horas só martelaria o seu servidor.

Entregas podem chegar fora de ordem ou repetidas. Trate cada uma como idempotente: ignore um order.posted que chegue depois de um order.delivered já processado.

Verificar a assinatura

Toda entrega vai assinada. Dois cabeçalhos:

CabeçalhoConteúdo
x-frete-signaturesha256=<hex>
x-frete-timestampsegundos desde a época Unix

A assinatura é o HMAC-SHA256 de <timestamp>.<corpo>, com o segredo devolvido no cadastro. O timestamp entra dentro do que é assinado — por isso alterá-lo invalida a assinatura, e uma requisição capturada não serve para sempre.

Três detalhes que quebram tudo

1. Use o corpo cru, antes de qualquer JSON.parse. Reserializar muda espaços e ordem de chaves, e a assinatura não fecha.

2. Rejeite timestamp fora de 5 minutos. Sem isso, quem capturar uma entrega pode reenviá-la para sempre.

3. Compare em tempo constante. Com ===, o tempo de resposta revela quantos bytes iniciais o atacante acertou.

Node.js

const { createHmac, timingSafeEqual } = require('crypto')

function verificar(segredo, corpoCru, assinatura, timestamp) {
  // 1. janela de 5 minutos
  if (!/^\d+$/.test(timestamp)) return false
  const agora = Math.floor(Date.now() / 1000)
  if (Math.abs(agora - Number(timestamp)) > 300) return false

  // 2. o timestamp entra no que é assinado
  const esperada = createHmac('sha256', segredo)
    .update(`${timestamp}.${corpoCru}`)
    .digest('hex')

  if (!assinatura.startsWith('sha256=')) return false
  const recebida = assinatura.slice('sha256='.length)
  if (recebida.length !== esperada.length) return false

  // 3. comparação em tempo constante
  return timingSafeEqual(
    Buffer.from(recebida, 'hex'),
    Buffer.from(esperada, 'hex'),
  )
}

PHP

function verificar($segredo, $corpoCru, $assinatura, $timestamp) {
  if (!ctype_digit($timestamp)) return false;
  if (abs(time() - (int)$timestamp) > 300) return false;

  $esperada = 'sha256=' . hash_hmac('sha256', "$timestamp.$corpoCru", $segredo);
  return hash_equals($esperada, $assinatura);
}

// o corpo cru, nunca $_POST
$corpoCru = file_get_contents('php://input');
O segredo aparece uma vez

Ele é devolvido apenas na resposta do cadastro. A listagem de webhooks nunca o traz de volta — se fosse legível depois, qualquer falha de autorização em uma tela permitiria forjar entregas assinadas. Perdeu? Cadastre outro webhook.

Rastreio

Formato do código

EC 00000001 4 BR — duas letras de serviço, oito dígitos sequenciais, um dígito verificador módulo 11 e o sufixo do país. O formato segue a convenção dos Correios para que leitores de código de barras e planilhas já existentes o aceitem; o código em si é nosso.

Em sandbox o formato é outro: SANDBOX seguido de 12 caracteres hexadecimais.

Código de sandbox não rastreia

Ele não passa na validação de formato: a consulta responde 422 CODIGO_INVALIDO e a página mostra erro. A página em si devolve HTTP 200 — ela é montada no navegador, e o erro aparece depois da consulta. Não use o código de status da página como verificação de saúde: 200 ali não significa que o rastreio existe.

Enquanto o token for frete_test_, todo link de rastreio que a sua loja mandar ao comprador vai parecer quebrado. É esperado, não é falha da integração.

Página para o seu cliente

https://app.martinslog.net/r/EC000000014BR

Aberta, sem login, e não mostra nome, documento nem endereço de ninguém — só serviço, situação e cidade/UF de cada etapa. É o link seguro para pôr no e-mail de confirmação.

Consulta programática

GET /api/rastreio/{codigo} sem token

Não exige autenticação, e por isso tem limite próprio: 30 consultas a cada 5 minutos por IP, com Retry-After na resposta 429.

HTTPCódigoSignifica
200{ "rastreio": { … } }
422CODIGO_INVALIDOFormato ou dígito verificador errado
404ENVIO_NAO_ENCONTRADOCódigo bem formado, envio inexistente
429LIMITE_CONSULTAS_EXCEDIDOLimite por IP estourado

O 422 é útil: significa que o código foi digitado errado e dá para corrigir. O 404 significa que o código é plausível mas não existe.

Cada evento traz sequencia, codigo, titulo, descricao, cidade, uf e ocorridoEm. Os códigos possíveis são ETIQUETA_EMITIDA, POSTADO, TRANSFERENCIA, AGUARDANDO_TRATAMENTO, SAIU_PARA_ENTREGA, TENTATIVA_FRUSTRADA, AGUARDANDO_RETIRADA, ENTREGUE, EXTRAVIADO, DEVOLUCAO_INICIADA e DEVOLVIDO.

Checklist de checkout

O que separa uma integração que funciona de uma que cobra duas vezes.

Grave o id do envio antes de qualquer outra coisa

Assim que o /cart responder, guarde o id no seu pedido e só chame o /cart quando esse campo estiver vazio. Duas chamadas para a mesma venda criam dois envios e duas cobranças na sua carteira.

Dispare a partir do pagamento confirmado, não do clique

O gatilho é o webhook do seu gateway confirmando o PIX ou o cartão. Criar o envio no clique de "finalizar compra" gera etiqueta para venda que não foi paga.

Trate 402 como estado, não como erro

Saldo insuficiente é rotina. O envio fica PENDING e nada se perde: avise o lojista, e tente de novo com o mesmo id depois da recarga. Não deixe o pedido travado em silêncio.

Prefira o webhook ao polling

Consultar /order/info de minuto em minuto consome a cota de 60/min que você vai precisar para vender. Use o webhook e reserve a consulta para conferência.

Confira sandbox em toda entrada

Tanto no webhook quanto no /order/info. Um pedido de teste tratado como venda real gera código de rastreio que não abre para o cliente.

Nunca mande preço

Não existe campo de preço em nenhum corpo de requisição. Se você enviar um, ele é descartado sem erro — e você vai passar horas procurando por que o valor "não foi aplicado".

O que ainda não existe

Limites reais da versão v0, listados para que você não os descubra em produção.

LimitaçãoConsequênciaComo contornar
Sem chave de idempotência /cart não aceita referência externa. Um campo external_id é descartado em silêncio. Deduplicar do seu lado, pelo id do envio gravado no pedido.
Sem endpoint em lote Importação de histórico é uma chamada por envio. Enfileirar respeitando 60/min.
Cadastro de webhook exige sessão Não dá para provisionar webhook por API. O lojista cadastra no painel, uma vez.
Saldo da carteira não é exposto Não há como checar saldo antes de faturar. Tratar o 402 como caso esperado.
Não há PDF de etiqueta A rota de etiqueta é de sessão e devolve só o código. O /checkout já emite; imprimir é pelo painel.
tracking_url é relativo Link quebrado se usado direto. Prefixar com a URL base.
LOST não tem evento Extravio chega como order.cancelled. Consultar o status quando o evento chegar.

MARTINS LOG E TRANSPORTES LTDA · Documento gerado a partir do código-fonte da plataforma. Endpoints, códigos de erro e limites verificados contra a API em produção.