Pular para o conteúdo principal

Integração Melhor Envio

Gateway de envio. Suporta Correios (PAC, SEDEX, Mini Envios), Jadlog, Loggi e outros. O frontend nunca fala diretamente com o Melhor Envio — todas as chamadas passam pelo backend.

Arquitetura

Conceitos-chave

ConceitoO que é
Endereço de origemArmazém/gráfica de onde saem os pedidos — vem das env vars MELHOR_ENVIO_FROM_*
Documento do destinatárioCPF/CNPJ vem automaticamente do cadastro do usuário. ME não permite remetente e destinatário com mesmo CPF
Dimensões do produtoArmazenadas no ProductVariant (weightGrams, heightCm, widthCm, lengthCm)
Etiqueta automáticaGerada automaticamente após pagamento aprovado — sem ação do admin
Provider genéricoOrderShipment usa campos genéricos (shipping_provider, provider_order_id, label_url) — facilita trocar de gateway no futuro

Dimensões no ProductVariant

Cada variante precisa de dimensões físicas para cálculo de frete:

CampoTipoExemploDescrição
weightGramsint300Peso em gramas
heightCmint4Altura em cm
widthCmint12Largura em cm
lengthCmint17Comprimento em cm

:::warning Fallback quando não preenchidas Se uma variante não tem dimensões, o backend usa valores padrão (300g, 2x15x20cm) e loga um warning. Configure as dimensões reais via POST/PATCH bulk. :::

POST /products/types/{id}/variants
{
"assetIds": ["uuid-size-350ml"],
"baseCostCents": 2500,
"weightGrams": 450,
"heightCm": 10,
"widthCm": 9,
"lengthCm": 9
}

1. Calcular Frete

POST /shipping/calculate

Sem autenticação. O backend carrega dimensões/peso do ProductVariant e monta o payload.

{
"toCep": "01018-020",
"items": [
{ "variantId": "uuid-da-variante", "quantity": 2 },
{ "variantId": "uuid-outra-variante", "quantity": 1 }
]
}
Response
{
"fromCep": "01310100",
"options": [
{
"id": 1,
"name": "PAC",
"companyName": "Correios",
"priceCents": 2693,
"deliveryMin": 7,
"deliveryMax": 9,
"carrier": "correios",
"serviceCode": "PAC",
"companyPicture": "https://..."
},
{
"id": 2,
"name": "SEDEX",
"companyName": "Correios",
"priceCents": 3588,
"deliveryMin": 3,
"deliveryMax": 4,
"carrier": "correios",
"serviceCode": "SEDEX"
},
{
"id": 3,
"name": ".Package",
"companyName": "Jadlog",
"priceCents": 2100,
"deliveryMin": 5,
"deliveryMax": 6,
"carrier": "jadlog",
"serviceCode": ".Package"
}
]
}

Opções com erro (transportadora não atende, peso excede, etc.) são removidas automaticamente.

2. Criar pedido com frete

O frontend envia a opção escolhida no payload do POST /orders:

{
"shipping": { "...": "endereço" },
"items": [{ "...": "itens" }],
"shippingCents": 2693,
"shippingMethodId": "1",
"carrier": "correios",
"serviceCode": "PAC"
}

shippingMethodId, carrier e serviceCode vêm diretamente da opção escolhida em /shipping/calculate.

3. Pipeline automática de etiqueta

Logo após o webhook do Asaas confirmar o pagamento (ou após dev-pay), o backend executa 5 chamadas sequenciais no ME. Tudo em best-effort — se algum passo falhar, o erro é logado mas o pedido permanece em approved.

Pipeline de etiqueta — disparada no payment approved
1
Criar envio (cart)Automático
POST /me/cart — cria envio no ME, retorna provider_order_id e delivery_max.
Salva providerOrderId + etaDays no OrderShipment (usa delivery_max do nível raiz, não delivery_range).
2
Comprar frete (checkout)Automático
POST /me/shipment/checkout — debita saldo do ME. Tenta extrair trackingCode (geralmente null).
3
Gerar etiquetaAutomático
POST /me/shipment/generate — ME monta o PDF da etiqueta. Segunda tentativa de extrair trackingCode.
4
Obter URL do PDFAutomático
POST /me/shipment/print — retorna labelUrl do PDF. Salva em OrderShipment.labelUrl.
5
Buscar trackingCodeAutomático
POST /me/shipment/tracking — só roda se trackingCode ainda é null após os passos anteriores.
Se ainda null, o tracking poller preenche depois (ver próxima seção).

Dados extraídos automaticamente

CampoDe onde vem
etaDaysdelivery_max do nível raiz do response do cart (passo 1). Atenção: delivery_range vem null
labelUrlResponse do print (passo 4)
trackingCodeTentado nos passos 2, 3 e 5 — se ainda null, fica a cargo do polling
Response do pedido após etiqueta gerada
{
"id": "uuid-do-pedido",
"paymentStatus": "approved",
"shipment": {
"id": "uuid-do-shipment",
"carrier": "correios",
"serviceCode": "PAC",
"shippingMethodId": "1",
"label": "PAC - Correios",
"priceCents": 2693,
"etaDays": 8,
"shippingProvider": "melhor_envio",
"providerOrderId": "me-uuid-xxx",
"labelUrl": "https://sandbox.melhorenvio.com.br/imprimir/xxx",
"trackingCode": null,
"status": "pending"
}
}

4. Tracking Polling (estratégia principal)

:::tip Polling > Webhook Polling é a estratégia principal de atualização de status — não o webhook. O webhook do ME é instável no sandbox e depende de configuração externa, então o polling é mais confiável e funciona em qualquer ambiente. :::

Um serviço roda a cada 30 minutos (container tracking-poller no docker-compose.yml) e consulta a API do ME para atualizar todos os shipments ativos.

Quais shipments são polled

  • Têm provider_order_id preenchido (ou seja: passo 1 da pipeline completou)
  • Status não é terminal (delivered ou returned)

O que é atualizado

CampoAtualização
statusMapeado do status ME
tracking_codePreenchido se ainda era null
shipped_at / delivered_atTimestamps quando muda para shipped/delivered
tracking_last_event_payloadJSON do polling (útil para debug)
OrderItem.trackingCodeCopiado do OrderShipment (sync automático — desde abril/2026)
OrderItem.fulfillmentStatusPropagado automaticamente do shipment.status

Endpoint admin — trigger manual

POST /shipping/poll-tracking

Requer JWT de admin. Dispara o polling sob demanda (em vez de esperar 30min).

Response
{
"total": 3,
"updated": 1,
"errors": 0,
"details": [
{
"me_order_id": "a18d7597-...",
"changed": true,
"me_status": "posted",
"our_status": "shipped",
"tracking": "PX12792017BR",
"old_status": "pending"
}
]
}

Rodando manualmente em dev

# Ver logs do poller
docker compose logs -f tracking-poller

# Executar uma vez dentro do container
docker exec labanana-api-web-1 python -m scripts.poll_tracking

Arquivos relevantes

ArquivoO que faz
app/routers/shipping.pypoll_active_shipments(), _poll_shipment(), endpoint POST /shipping/poll-tracking
app/crud/order.pyget_active_shipments() — query dos shipments ativos
scripts/poll_tracking.pyScript standalone usado pelo cron
docker-compose.ymlServiço tracking-poller

5. Webhook (bonus)

Endpoint: POST /webhooks/melhor-envio

Quando funciona, atualiza o status em tempo real — mas o polling é a fonte de verdade.

Cadastro no painel ME

  1. Integrações > Área Dev
  2. Cadastrar Aplicativo (se ainda não tiver)
  3. Dentro do app, Novo Webhook
  4. URL: {API_URL}/webhooks/melhor-envio (em dev, usar URL do tunnel)
  5. Marcar o evento "Atualização das etiquetas criadas e editadas"

:::warning Mesmo token As etiquetas precisam ser geradas usando o mesmo token/aplicativo onde o webhook está configurado, senão o webhook não dispara. :::

Payload recebido

{
"event": "order.posted",
"data": {
"id": "me-order-uuid",
"protocol": "ORD-2024XXXXXXXXXX",
"status": "posted",
"tracking": "RASTREIO123BR",
"tracking_url": "https://www.melhorrastreio.com.br/rastreio/RASTREIO123BR"
}
}
  • Header de verificação: X-ME-Signature — HMAC-SHA256 do body usando o Secret do aplicativo como chave
  • Retry: 5 tentativas com intervalo de 15min, timeout de 6s por request

Mapeamento de status

Melhor EnvioLabanana ShipmentStatus
pendingpending
releasedpending
postedshipped
in_transitin_transit
delivereddelivered
canceledreturned
undeliveredreturned

Use o Melhor Rastreio:

https://www.melhorrastreio.com.br/rastreio/{trackingCode}

O frontend monta o link a partir do trackingCode retornado em OrderShipmentResponse.

Configuração .env

Sandbox
MELHOR_ENVIO_TOKEN=eyJ0eXAi...
MELHOR_ENVIO_BASE_URL=https://sandbox.melhorenvio.com.br/api/v2

# Endereço do remetente (armazém/gráfica)
MELHOR_ENVIO_FROM_CEP=01310100
MELHOR_ENVIO_FROM_NAME=Labanana
MELHOR_ENVIO_FROM_PHONE=+5519999999999
MELHOR_ENVIO_FROM_EMAIL=contact@labanana.art
MELHOR_ENVIO_FROM_DOCUMENT=12345678000199
MELHOR_ENVIO_FROM_ADDRESS=Rua Exemplo
MELHOR_ENVIO_FROM_NUMBER=123
MELHOR_ENVIO_FROM_COMPLEMENT=Sala 1
MELHOR_ENVIO_FROM_DISTRICT=Centro
MELHOR_ENVIO_FROM_CITY=Campinas
MELHOR_ENVIO_FROM_STATE=SP

Campos obrigatórios do remetente: phone, address, city são exigidos pelo ME para Jadlog (service 3). Os demais são recomendados para todas as transportadoras.

:::danger Guardrail de produção O app recusa iniciar em ENVIRONMENT=prod se MELHOR_ENVIO_TOKEN ou MELHOR_ENVIO_FROM_CEP estão vazios. :::

Ambientes

AmbienteBase URLSaldo
Sandboxhttps://sandbox.melhorenvio.com.br/api/v2R$ 10.000 fictícios
Produçãohttps://melhorenvio.com.br/api/v2Saldo real da carteira ME

:::info Ciclo de vida no sandbox

  • ~15min após gerar etiqueta → status muda para posted + webhook disparado
  • ~15min após postagem → status muda para delivered + webhook disparado
  • trackingCode é atribuído quando muda para posted

Apenas Correios e Jadlog estão disponíveis no sandbox. :::

Cálculo do pacote (MVP)

Volume único por pedido:

  • Peso total = soma de weight_grams × quantity de cada item
  • Dimensões = maior altura, largura e comprimento entre todos os itens

Simplificado — no futuro, implementar lógica de empacotamento (múltiplos volumes, envelope vs caixa).

Fluxo completo resumido

1. Cliente digita CEP na página do produto/carrinho
→ POST /shipping/calculate
→ mostra opções com preço e prazo

2. Cliente escolhe opção e vai pro checkout
→ POST /orders com shippingMethodId + shippingCents

3. Cliente paga (Asaas — Pix/cartão/boleto)

4. Webhook Asaas → payment approved
→ Pipeline automática de etiqueta (cart → checkout → generate → print → tracking)
→ Salva labelUrl + trackingCode no OrderShipment

5. Admin imprime etiqueta (PDF no labelUrl)
→ Posta o pacote

6. Tracking poller (a cada 30min)
→ Atualiza status: pending → shipped → in_transit → delivered
→ Preenche trackingCode se ainda null
→ Propaga para OrderItem.fulfillmentStatus e OrderItem.trackingCode

7. Webhook ME (bonus, quando funcionar)
→ Atualiza em tempo real

Troubleshooting

ProblemaCausaSolução
Cálculo retorna 0 opçõesCEP inválido ou sem coberturaVerificar CEP de destino
Frete muito caroDimensões/peso incorretosVerificar weightGrams, heightCm, etc.
Etiqueta não gerouSaldo ME insuficiente ou dados incompletosVerificar logs e saldo no painel ME
MELHOR_ENVIO_TOKEN not configuredToken vazio no .envAdicionar MELHOR_ENVIO_TOKEN
Dimensões fallback nos logsVariante sem dimensões preenchidasAtualizar via PATCH bulk
trackingCode null após etiquetaME não atribui imediatamenteAguardar polling ou chamar POST /shipping/poll-tracking
Poller não atualizaShipment sem provider_order_idVerificar se pipeline de etiqueta completou