CT-e e MDF-e¶
Os documentos do transporte compartilham quase toda a infraestrutura da NF-e — chave de acesso, assinatura, cliente SOAP — e diferem no que descrevem.
| CT-e (57) | CT-e OS (67) | MDF-e (58) | |
|---|---|---|---|
| O que documenta | Uma prestação de transporte de carga | Uma prestação que não move carga | O conjunto de documentos dentro de um veículo |
| Quem emite | A transportadora | A transportadora | Quem transporta, próprio ou de terceiros |
| Conteúdo central | Carga, componentes do frete, ICMS | Serviço em texto livre, veículo, ICMS | Documentos por município de descarregamento, veículo, condutores |
| Ciclo de vida | Autorizar → corrigir → cancelar | Autorizar → corrigir → cancelar | Autorizar → encerrar |
| Leiaute | 4.00 | 4.00 | 3.00 |
| Pacote | cte |
cteos |
mdfe |
O MDF-e precisa ser encerrado
Um manifesto em aberto bloqueia a emissão do seguinte. O encerramento é o evento que fecha a viagem, e é a causa número um de operação parada no pátio. Trate-o como parte obrigatória do fluxo, não como opcional.
CT-e¶
c := cte.Novo(cte.ModalRodoviario)
ide := &c.InfCte.Ide
ide.CFOP = "5353"
ide.NatOp = "PRESTACAO DE SERVICO DE TRANSPORTE"
ide.Serie, ide.NCT = 1, 987
ide.DhEmi = tipos.AgoraEm(uf.RS.Fuso())
ide.TpAmb = cte.Homologacao
ide.CMunEnv, ide.XMunEnv, ide.UFEnv = 4314902, "PORTO ALEGRE", "RS"
ide.CMunIni, ide.XMunIni, ide.UFIni = 4314902, "PORTO ALEGRE", "RS"
ide.CMunFim, ide.XMunFim, ide.UFFim = 4305108, "CAXIAS DO SUL", "RS"
ide.IndIEToma = cte.ContribuinteICMS
ide.Toma3 = &cte.Toma3{Toma: cte.TomadorRemetente}
c.InfCte.Emit = cte.Emit{ /* a transportadora */ }
c.InfCte.Rem = &cte.Rem{ /* quem envia a carga */ }
c.InfCte.Dest = &cte.Dest{ /* quem recebe */ }
// O valor total sai da soma dos componentes.
c.InfCte.VPrest.Comp = []cte.Componente{
{XNome: "FRETE PESO", VComp: tipos.D("850.00")},
{XNome: "PEDAGIO", VComp: tipos.D("72.50")},
{XNome: "TAXA DE COLETA", VComp: tipos.D("77.50")},
}
base := tipos.D("1000.00")
c.InfCte.Imp.ICMS.ICMS00 = &cte.ICMS00{
CST: "00", VBC: base, PICMS: tipos.D("12.00"),
VICMS: base.Percentual(tipos.D("12.00"), 2),
}
c.InfCte.InfCTeNorm.InfCarga = cte.InfCarga{
ProPred: "BEBIDAS",
InfQ: []cte.InfQ{
{CUnid: cte.UnidadeKG, TpMed: "PESO BRUTO", QCarga: tipos.D("12500.0000")},
},
}
c.InfCte.InfCTeNorm.InfDoc = &cte.InfDoc{
InfNFe: []cte.InfNFe{{Chave: chaveDaNota}},
}
c.InfCte.InfCTeNorm.InfModal.Rodo = &cte.Rodo{RNTRC: "12345678"}
assinado, err := c.AssinarCom(cert)
O tomador¶
Quem paga o frete é declarado de duas formas, e o leiaute não aceita as duas:
// Quando o tomador é uma das partes já identificadas:
ide.Toma3 = &cte.Toma3{Toma: cte.TomadorRemetente}
// Quando é um terceiro:
ide.Toma4 = &cte.Toma4{
Toma: cte.TomadorOutros,
CNPJ: "11222333000181", XNome: "TOMADOR TERCEIRO LTDA",
}
Validar confere que a parte apontada em toma3 foi realmente preenchida — um
CT-e que diz "o tomador é o expedidor" sem grupo exped é rejeitado pela SEFAZ.
O modal rodoviário no 4.00¶
No leiaute 4.00 o grupo rodo se resume ao RNTRC e às ordens de coleta. Veículo,
placa e condutor saíram do CT-e: eles agora vivem no MDF-e. Se você está
migrando de uma implementação da versão 3.00, é aqui que o código muda mais.
Transmissão¶
A recepção do 4.00 é síncrona e recebe um documento por vez, comprimido:
cliente, _ := sefaz.NovoClienteCTe(sefaz.ConfigCTe{
UF: uf.RS, Ambiente: cte.Homologacao, Certificado: cert,
})
resposta, err := cliente.Autorizar(ctx, assinado)
if resposta.ProtCTe.Autorizado() {
proc, _ := cte.MontarCTeProc(assinado, resposta.ProtCTe)
}
A compressão é feita por cte.MontarEnvioSincrono e não altera os bytes
assinados — a assinatura continua conferindo do outro lado.
Eventos¶
Três eventos, transmitidos um a um — o CT-e não tem lote:
canc, _ := cte.NovoCancelamento(cte.DadosCancelamento{
Chave: c.Chave(), CNPJ: cnpj, Ambiente: cte.Producao, UF: uf.RS,
Protocolo: prot.InfProt.NProt,
Justificativa: "Conhecimento emitido com o tomador errado",
})
assinado, _ := canc.AssinarCom(cert)
ret, _ := cliente.EnviarEvento(ctx, assinado)
O cancelamento só é aceito antes do início da prestação e dentro do prazo da UF, em geral 168 horas. Passado isso, o caminho é o CT-e de anulação seguido de um substituto.
A carta de correção do CT-e é mais estrita que a da NF-e: em vez de um texto livre, ela exige que cada correção aponte o grupo, o campo e o novo valor.
cc, _ := cte.NovaCartaCorrecao(cte.DadosCartaCorrecao{
Chave: c.Chave(), CNPJ: cnpj, Ambiente: cte.Producao, UF: uf.RS,
Correcoes: []cte.Correcao{
{GrupoAlterado: "ide", CampoAlterado: "xMunFim", ValorAlterado: "BENTO GONCALVES"},
},
})
O campo xCondUso, cujo texto a SEFAZ compara caractere a caractere, é
preenchido sozinho a partir de cte.CondicaoDeUsoCCe.
O terceiro evento é do outro lado do balcão: quem registra a prestação em desacordo é o tomador, não o emitente.
des, _ := cte.NovoDesacordo(cte.DadosDesacordo{
Chave: chaveDoCTe, CNPJ: cnpjDoTomador, Ambiente: cte.Producao, UF: uf.RS,
Observacao: "Carga entregue com avaria em tres volumes",
})
CT-e OS¶
O CT-e OS, modelo 67, documenta a prestação que não move carga: transporte
de pessoas, transporte de valores e excesso de bagagem. Ele tem raiz própria,
<CTeOS>, e não é uma variação do 57 — onde o CT-e descreve remetente,
destinatário e carga, o CT-e OS descreve um tomador e um serviço em texto livre.
c := cteos.Novo(cteos.ServicoTransportePessoas)
ide := &c.InfCte.Ide
ide.CFOP = "5357"
ide.NatOp = "PRESTACAO DE SERVICO DE TRANSPORTE DE PESSOAS"
ide.Serie, ide.NCT = 1, 432
ide.DhEmi = tipos.AgoraEm(uf.RS.Fuso())
ide.TpAmb = cte.Homologacao
ide.CMunEnv, ide.XMunEnv, ide.UFEnv = 4314902, "PORTO ALEGRE", "RS"
ide.CMunIni, ide.XMunIni, ide.UFIni = 4314902, "PORTO ALEGRE", "RS"
ide.CMunFim, ide.XMunFim, ide.UFFim = 4305108, "CAXIAS DO SUL", "RS"
ide.IndIEToma = cte.ContribuinteICMS
c.InfCte.Emit = cte.Emit{ /* a transportadora */ }
// Um tomador só, declarado uma vez. Não há toma3 nem toma4.
c.InfCte.Toma = &cteos.Toma{
CNPJ: "11222333000181", IE: "1234567890",
XNome: "EMPRESA CONTRATANTE SA",
}
c.InfCte.VPrest.Comp = []cte.Componente{
{XNome: "SERVICO DE FRETAMENTO", VComp: tipos.D("2400.00")},
{XNome: "PEDAGIO", VComp: tipos.D("100.00")},
}
// O serviço é descrito em texto, no lugar da carga do modelo 57.
c.InfCte.InfCTeNorm.InfServico = cteos.InfServico{
XDescServ: "FRETAMENTO EVENTUAL DE ONIBUS PARA EXCURSAO",
InfQ: &cteos.InfQ{QCarga: tipos.D("42")}, // passageiros
}
// O veículo volta ao documento: no transporte de pessoas não há MDF-e.
c.InfCte.InfCTeNorm.InfModal.RodoOS = &cteos.RodoOS{
TAF: "1234567890",
Veic: &cteos.Veiculo{Placa: "ABC1D23", RENAVAM: "12345678901", UF: "RS"},
InfFretamento: &cteos.InfFretamento{
TpFretamento: cteos.FretamentoEventual,
DhViagem: tipos.Ptr(tipos.DH("2026-03-06T05:00:00-03:00")),
},
}
assinado, err := c.AssinarCom(cert)
O que vem do pacote cte¶
Tudo o que os dois modelos têm em comum não é redefinido: os grupos de ICMS, o endereço, o emitente, o valor da prestação, as informações complementares, a cobrança, o responsável técnico e o protocolo. Por isso o código acima importa os dois pacotes.
Transmissão¶
O mesmo serviço atende aos dois modelos, e sefaz.ClienteCTe reconhece qual é
pelo elemento raiz do documento assinado:
resposta, err := cliente.Autorizar(ctx, assinado)
if resposta.ProtCTe.Autorizado() {
proc, _ := cteos.MontarCTeOSProc(assinado, resposta.ProtCTe)
}
Eventos¶
Os eventos são os mesmos dos dois modelos — o elemento raiz é eventoCTe em
ambos, e o que muda é só a chave referenciada. Use as funções do pacote cte:
canc, _ := cte.NovoCancelamento(cte.DadosCancelamento{
Chave: c.Chave(), // a chave é de modelo 67; o evento aceita
CNPJ: cnpj, Ambiente: cte.Producao, UF: uf.RS,
Protocolo: prot.InfProt.NProt, Justificativa: "…",
})
Este pacote é novo
O conjunto de campos segue o leiaute 4.00, mas o cteos tem menos rodagem
em campo que o cte. Homologue com prestações reais do seu cenário antes de
emitir com valor fiscal. Um grupo fora de ordem é rejeitado na validação de
esquema, com a mensagem apontando o elemento — falha clara, não silenciosa.
O DACTE OS em PDF está em Documentos auxiliares, com o mesmo aviso de fidelidade dos demais.
MDF-e¶
m := mdfe.Novo(mdfe.ModalRodoviario)
ide := &m.InfMDFe.Ide
ide.TpAmb = mdfe.Homologacao
ide.TpEmit = mdfe.EmitentePrestadorServico
ide.Serie, ide.NMDF = 1, 55
ide.DhEmi = tipos.AgoraEm(uf.RS.Fuso())
ide.UFIni, ide.UFFim = "RS", "RS"
ide.InfMunCarrega = []mdfe.InfMunCarrega{
{CMunCarrega: 4314902, XMunCarrega: "PORTO ALEGRE"},
}
m.InfMDFe.Emit = mdfe.Emit{ /* quem transporta */ }
// O veículo e os condutores ficam no modal.
m.InfMDFe.InfModal.Rodo = &mdfe.Rodo{
InfANTT: &mdfe.InfANTT{RNTRC: "12345678"},
VeicTracao: mdfe.VeicTracao{
Placa: "ABC1D23", Tara: 8500, CapKG: 22000,
TpRod: mdfe.RodadoCavaloMec, TpCar: mdfe.CarroceriaFechadaBau, UF: "RS",
Condutor: []mdfe.Condutor{{XNome: "JOAO DA SILVA", CPF: "52998224725"}},
},
}
// Os documentos vão agrupados por onde serão descarregados.
m.InfMDFe.InfDoc.InfMunDescarga = []mdfe.InfMunDescarga{
{
CMunDescarga: 4305108, XMunDescarga: "CAXIAS DO SUL",
InfNFe: []mdfe.InfNFe{{ChNFe: chaveA}, {ChNFe: chaveB}},
},
{
CMunDescarga: 4313409, XMunDescarga: "NOVO HAMBURGO",
InfCTe: []mdfe.InfCTe{{ChCTe: chaveDoCTe}},
},
}
m.InfMDFe.Tot = mdfe.Tot{
VCarga: tipos.D("87500.00"),
CUnid: mdfe.UnidadeKG,
QCarga: tipos.D("18400.0000"),
}
assinado, err := m.AssinarCom(cert)
Preparar conta os documentos e preenche qNFe, qCTe e qMDFe sozinho. O
valor e o peso da carga não são calculados: eles não saem dos documentos
relacionados, e sim da pesagem e da nota de quem embarca.
Transmissão¶
A recepção do 3.00 também é síncrona e comprimida. O MDF-e é centralizado: não há autorizador estadual, todas as unidades da federação são atendidas pela Sefaz Virtual do Rio Grande do Sul.
cliente, _ := sefaz.NovoClienteMDFe(sefaz.ConfigMDFe{
UF: uf.RS, Ambiente: mdfe.Homologacao, Certificado: cert,
})
resposta, err := cliente.Autorizar(ctx, assinado)
if resposta.ProtMDFe.Autorizado() {
proc, _ := mdfe.MontarMDFeProc(assinado, resposta.ProtMDFe)
}
Descobrindo o que travou a emissão¶
Quando a SEFAZ rejeita a autorização por manifesto em aberto, ela não diz qual. Esta consulta diz:
pendentes, err := cliente.NaoEncerrados(ctx, cnpj)
for _, chave := range pendentes.Chaves() {
log.Printf("encerrar antes de emitir: %s", chave)
}
Vale chamá-la antes de emitir, não só depois de apanhar.
Encerrando a viagem¶
enc, err := mdfe.NovoEncerramento(mdfe.DadosEncerramento{
Chave: m.Chave(),
CNPJ: cnpj,
Ambiente: mdfe.Producao,
Protocolo: prot.InfProt.NProt,
UF: uf.RS,
CodigoMunicipio: 4305108, // onde a viagem realmente terminou
})
assinado, _ := enc.AssinarCom(cert)
O município informado é onde a viagem terminou de fato, que pode não ser o destino previsto — um caminhão que descarrega tudo antes do último ponto encerra onde parou.
Os outros eventos seguem o mesmo padrão:
mdfe.NovoCancelamento(...) // só antes de a viagem começar
mdfe.NovaInclusaoCondutor(...) // troca de motorista em viagem longa
Todos são transmitidos um a um — o MDF-e não tem lote de eventos:
O encerramento tem código próprio
Os eventos do MDF-e são aceitos com cStat 135, mas o encerramento
responde 132. Registrado() já cobre os dois; se você conferir o código
à mão, não esqueça do 132.
Os documentos auxiliares¶
O DACTE e o DAMDFE saem do mesmo pacote que o DANFE, a partir do XML de distribuição:
dacte, err := danfe.GerarDACTE(procCTe, danfe.Opcoes{})
damdfe, err := danfe.GerarDAMDFE(procMDFe, danfe.Opcoes{})
Os detalhes de cada leiaute estão em Documentos auxiliares.
Estado da implementação¶
| Item | Situação |
|---|---|
| CT-e modelo 57, leiaute 4.00 | Modelo completo, modal rodoviário completo |
| CT-e — eventos | Cancelamento, carta de correção e prestação em desacordo |
| CT-e — demais modais | Estruturas presentes, sem rodagem |
| CT-e OS, modelo 67 | Modelo completo, sem rodagem em campo |
| MDF-e modelo 58, leiaute 3.00 | Modelo completo, modal rodoviário completo |
| MDF-e — eventos | Encerramento, cancelamento e inclusão de condutor |
| DACTE, DACTE OS e DAMDFE em PDF | Completos — veja Documentos auxiliares |
| Cliente SEFAZ do CT-e e do CT-e OS | Status, autorização e consulta |
| Cliente SEFAZ do MDF-e | Status, autorização, consulta, eventos e não encerrados |
Endereços dos serviços
Vale o mesmo aviso da NF-e, e com mais força: CT-e e MDF-e têm tabelas de endereço próprias e menos verificadas em campo. Rode a consulta de status da sua UF antes de emitir, e sobreponha o que divergir: