DCTFWeb: Transmissão
Endpoints da WebApiAlcance que entregam a declaração da DCTFWeb à Receita Federal pelo Integra Contador (Serpro), serviço TRANSDECLARACAO310, mais o histórico de tentativas e a consulta da situação da declaração no fisco.
É a única superfície da DCTFWeb que escreve no fisco. Todo o resto do domínio - Guias, Recibos, Emissão e Catálogos - consulta, coleta ou registra. Aqui a operação é irreversível: não existe rota que cancele ou desfaça uma transmissão.
Todos os endpoints exigem autenticação. Veja Autenticação para o fluxo de API Key e JWT. O Swagger oficial está em api.contabilidadealcance.com.br/docs (tags DCTFWeb: Transmissão e DCTFWeb: Situação).
Operação irreversível - leia a regra do estado INDETERMINADO antes de integrar
POST /dctfweb/transmissao/ entrega uma declaração à Receita em nome do cliente. Quando o /Declarar não devolve veredito (timeout ou 5xx sem código), a API responde 504 com estado indeterminado: a declaração pode ter entrado.
A regra de consumo é absoluta: não retransmita. Consulte GET /dctfweb/situacao (ou repita o POST, que consulta o recibo antes de decidir). Reenviar às cegas é exatamente como se cria uma declaração duplicada no fisco. Detalhes em Estado INDETERMINADO.
Base path
/api/v1/dctfweb/transmissaoA consulta de situação fica fora desse sub-prefixo, em /api/v1/dctfweb/situacao: ela é leitura pura e não faz parte do recurso "transmissão".
Escopos necessários
dctfweb:write-POST /dctfweb/transmissao/ePOST /dctfweb/transmissao/simular.dctfweb:read-GET /dctfweb/transmissao/,GET /dctfweb/transmissao/{transmissao_id}eGET /dctfweb/situacao.
O recurso vem do primeiro segmento do path (dctfweb) e a ação, do método HTTP. Estes caminhos nasceram sob o prefixo consolidado: não existe alias legado para eles. Tokens emitidos antes da consolidação não carregam dctfweb:read / dctfweb:write - relogue ou atualize a API Key.
Transmitir e simular exigem perfil de gestão E sessão de usuário
As duas rotas POST acumulam três camadas de autorização, que respondem a perguntas diferentes:
- Escopo
dctfweb:write- derivado do path, como em todo o resto da API. - Perfil
administradoroudiretoria. Um token com o escopo correto mas perfil operacional é recusado com403. - Credencial: exigem sessão de usuário (JWT de login). Uma API Key é recusada com
403, mesmo com escopo e perfil corretos - a mensagem é"<ação> não pode ser disparada por API Key — exige sessão de usuário (login).". Transmitir é ato fiscal irreversível que fica registrado com a autoria de quem o disparou, e uma credencial de integração não corresponde a uma pessoa.
Consequência prática: automação sem usuário não transmite DCTFWeb. As rotas de leitura (GET) não têm essa restrição e funcionam normalmente com API Key.
O CNPJ precisa ser de um cliente do escritório
Nas cinco rotas, o documento informado é resolvido contra a carteira interna (busca por dígitos, porque cpfecnpj está em formato misto na base). CNPJ que não pertence a nenhum cliente cadastrado recebe 403:
"O documento informado não pertence a nenhum cliente do escritório. A transmissão de DCTFWeb só é permitida para clientes cadastrados."
Isso vale inclusive para a consulta de situação - situação fiscal de terceiro é vazamento mesmo em leitura, e mesmo para um administrador.
Envelope
As cinco rotas usam o envelope padrão da WebApi:
{
"status": "success",
"message": "Mensagem em PT-BR",
"data": {}
}Erros têm duas formas, e a diferença importa para quem trata a resposta:
| Origem do erro | Corpo |
|---|---|
Validação do corpo pelo FastAPI/Pydantic (POST) | { "detail": [ { "loc": [...], "msg": "...", "type": "..." } ] } |
Qualquer HTTPException da aplicação (inclusive Serpro) | { "message": <detalhe> } - o detalhe pode ser string, objeto ou lista |
A segunda forma é o handler global de erros da WebApi: ele serializa o detail sob a chave message, não detail. Nas recusas vindas do Serpro, message é um objeto (ver Erros do Serpro); na validação dos query params de GET /dctfweb/situacao, é uma lista de { "loc", "msg" }.
Como a transmissão funciona
Uma chamada a POST /dctfweb/transmissao/ executa, do lado do servidor, três etapas contra o Integra Contador - o consumidor da API não faz nenhuma delas:
| # | Etapa | O que acontece |
|---|---|---|
| 1 | CONSXMLDECLARACAO38 (acionamento /Consultar) | Busca na Receita o XML da declaração do período (XMLStringBase64). |
| 2 | Assinatura local (XMLDSig) | Assina o XML com o certificado A1 do escritório. Roda fora do event loop. |
| 3 | TRANSDECLARACAO310 (acionamento /Declarar) | Envia o XML assinado - é o passo que entrega a declaração. |
Depois do sucesso vem o encadeamento, na mesma operação: CONSRECIBO32 (recibo de entrega, com PDF) + GERARGUIA31 (DARF). Os artefatos são gravados nos registros internos de Recibos e Guias, e seus IDs voltam na resposta.
Falha no encadeamento NÃO invalida a transmissão
Se o recibo ou a guia falharem, a declaração continua entregue. O desfecho do encadeamento vem em um campo separado (encadeamento.estado: ok | parcial | falha | nao_executado), justamente para que ninguém leia "não consegui baixar o DARF" como "preciso transmitir de novo". GUIA03 ("não há débitos com saldo a pagar") é inclusive um desfecho normal para quem não tem o que pagar.
A operação é pesada e síncrona
Uma transmissão bem-sucedida faz até 4 chamadas ao Serpro (XML, /Declarar, recibo, guia), cada uma com timeout próprio de 60s por padrão. A requisição só responde quando tudo termina - dimensione o timeout do seu cliente HTTP de acordo. Não há fila, callback nem polling: o resultado vem no corpo da resposta.
Endpoints
POST /dctfweb/transmissao/
Transmite (ou retransmite) a declaração da DCTFWeb do período informado. Responde 200 OK - inclusive quando o desfecho é "já estava transmitida".
Parâmetros
Sem parâmetros de rota ou query - os dados vão no corpo (DctfwebTransmitirRequest, ver DctfwebTransmitirRequest).
Request
{
"cnpj": "27898481000150",
"categoria": "GERAL_MENSAL",
"anoPA": "2026",
"mesPA": "04",
"idsSistemaOrigem": [1, 6, 7]
}Response 200 OK
{
"status": "success",
"message": "Transmissão processada.",
"data": {
"id": 118,
"id_cliente": 123,
"cnpj": "27898481000150",
"categoria": "GERAL_MENSAL",
"competencia": "2026-04",
"modo": "real",
"estado": "transmitida",
"mensagem": "Declaração transmitida com sucesso à Receita Federal.",
"codigo_serpro": null,
"classe_desfecho": null,
"numero_recibo": "12.34.56.78-90",
"encadeamento": {
"estado": "ok",
"numero_recibo": "12.34.56.78-90",
"id_recibo": 5120,
"id_guia": 8830,
"detalhe": null
},
"payload_simulado": null
}
}O campo estado é o que decide o que fazer em seguida:
estado | HTTP | Significado | O que fazer |
|---|---|---|---|
transmitida | 200 | O /Declarar aceitou nesta execução. | Nada. Recibo e guia vêm em encadeamento. |
ja_transmitida | 200 | A declaração já estava entregue - via guard local/remoto ou via TRANS11. Não é erro. | Nada. É o estado desejado. |
indeterminado | 504 | O /Declarar não respondeu. Pode ter entrado. | NÃO retransmita. Consulte GET /dctfweb/situacao. |
falha | 422, 500, 502 | Recusa com veredito conhecido (negócio, assinatura, entrada inválida). | Corrigir a causa. O código do Serpro vem na resposta. |
transmitida e ja_transmitida não foram fundidas de propósito: as duas significam "a declaração está entregue", mas só a primeira significa "foi esta execução que a entregou". A auditoria depende dessa diferença.
`forcar_retransmissao` serve a UM cenário
O default é false, e nesse modo o guard barra a operação quando o período já consta entregue (respondendo ja_transmitida, sem reenviar nada). forcar_retransmissao: true segue mesmo assim - é o caminho do cenário real de retransmissão: a apuração foi reaberta (tipicamente para corrigir a EFD-Reinf), reencerrada, e a declaração precisa voltar a ATIVA.
Ele não libera o estado indeterminado. Nesse caso, forçar não tem efeito nenhum: a dúvida só se resolve por consulta de recibo.
Escopo: dctfweb:write + perfil administrador ou diretoria + sessão de usuário (API Key recusada)
POST /dctfweb/transmissao/simular
DRY-RUN. Executa as etapas 1 e 2 (consulta o XML na Receita e assina com o certificado A1), monta o payload do TRANSDECLARACAO310 e para antes do envio. Responde 200 OK.
Serve para conferir exatamente o que seria enviado sem declarar nada: qual período, qual recibo alvo, qual categoria, e se o XML da apuração já existe e é assinável. É a forma de validar o fluxo inteiro contra produção sem tocar no fisco.
O modo é fixado no SERVIDOR
A simulação é uma rota própria, e não um booleano do corpo. Não existe campo em DctfwebTransmitirRequest capaz de fazer /simular transmitir, nem de fazer POST /dctfweb/transmissao/ simular - a decisão é tomada no servidor, antes de qualquer chamada ao /Declarar.
É essa separação que torna a garantia verificável: nenhum erro de preenchimento do corpo transforma um dry-run em uma declaração entregue.
Parâmetros
Sem parâmetros de rota ou query - o corpo é o mesmo DctfwebTransmitirRequest da transmissão real. forcar_retransmissao é aceito, mas irrelevante aqui: a simulação não passa pelos guards de idempotência, de propósito - o dry-run precisa continuar disponível justamente quando o período já está transmitido, que é o caso da conferência de uma retransmissão.
Request
{
"cnpj": "27898481000150",
"categoria": "GERAL_MENSAL",
"anoPA": "2026",
"mesPA": "04"
}Response 200 OK
{
"status": "success",
"message": "Simulação concluída — nada foi transmitido à Receita.",
"data": {
"id": 117,
"id_cliente": 123,
"cnpj": "27898481000150",
"categoria": "GERAL_MENSAL",
"competencia": "2026-04",
"modo": "simulacao",
"estado": "simulada",
"mensagem": "Simulação concluída. O XML foi consultado e assinado, e o payload abaixo é EXATAMENTE o que seria enviado — nada foi transmitido à Receita.",
"codigo_serpro": "DRY-RUN",
"classe_desfecho": null,
"numero_recibo": null,
"encadeamento": null,
"payload_simulado": {
"idSistema": "DCTFWEB",
"idServico": "TRANSDECLARACAO310",
"acionamento": "/Declarar",
"contribuinte": { "tipo": 2, "numero": "27898481000150" },
"dados": {
"categoria": "GERAL_MENSAL",
"anoPA": "2026",
"mesPA": "04"
},
"xml_assinado": {
"tamanho": 412880,
"sha256": "9f2a1c7d3b5e0a44",
"preview": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48UHJvY0RjdGY"
},
"xml_original": {
"tamanho": 401220,
"sha256": "3b77de91c04a5f28",
"preview": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48UHJvY0RjdGY"
}
}
}
}Observe que payload_simulado.dados traz o payload completo, exceto xmlAssinadoBase64 - o XML assinado da DCTFWeb tem de centenas de KB a megabytes, e devolvê-lo estouraria payload, log e tela sem ajudar ninguém. No lugar dele vem um resumo verificável (tamanho + sha256 + preview), que serve para comparar duas execuções e confirmar que o XML existe e mudou (ou não).
Uma simulação é registrada no histórico, com modo: "simulacao" e estado: "simulada", e é excluída do guard de idempotência - um dry-run não prova nada sobre o que a Receita tem registrado.
Escopo: dctfweb:write + perfil administrador ou diretoria + sessão de usuário (API Key recusada)
GET /dctfweb/transmissao/
Histórico de transmissões (trilha de auditoria fiscal), no envelope paginado, com filtros e ordenação. Um registro por tentativa - inclusive as simuladas e as que falharam.
Parâmetros de query
| Parâmetro | Tipo | Default | Observações |
|---|---|---|---|
id_cliente | int | - | Filtra por cliente. |
id_usuario | int | - | Filtra por autoria (quem disparou). |
categoria | str | - | Ex.: GERAL_MENSAL. Case-insensitive, ignora espaços nas pontas. |
competencia | str | - | AAAA-MM; AAAA nas categorias de 13º salário. Comparação exata. |
estado | str | - | simulada | transmitida | ja_transmitida | indeterminado | falha. Case-insensitive. |
modo | str | - | real | simulacao. Case-insensitive. |
start_date | datetime | - | data_hora_inicio >= start_date. |
end_date | datetime | - | data_hora_inicio <= end_date. |
sort_by | str (whitelist) | data_hora_inicio | Whitelist na seção Ordenação, abaixo. Valor fora dela → 422. |
sort_dir | asc | desc | desc | Valor fora da whitelist → 422. |
page | int (≥ 1) | 1 | Página, 1-based. |
per_page | int (1-500) | - | Itens por página. Se omitido, retorna todos (sem paginar). |
Request
GET /api/v1/dctfweb/transmissao/?id_cliente=123&estado=indeterminado&page=1&per_page=20Response 200 OK
{
"status": "success",
"message": "Transmissões recuperadas com sucesso!",
"data": {
"items": [
{
"id": 118,
"id_cliente": 123,
"id_usuario": 7,
"atividade_id": 90514,
"categoria": "GERAL_MENSAL",
"competencia": "2026-04",
"modo": "real",
"estado": "transmitida",
"numero_recibo": "12.34.56.78-90",
"codigo_serpro": null,
"classe_desfecho": null,
"mensagem": "Declaração transmitida com sucesso à Receita Federal.",
"id_recibo": 5120,
"id_guia": 8830,
"encadeamento_estado": "ok",
"data_hora_inicio": "2026-08-05T09:12:44-03:00",
"data_hora_desfecho": "2026-08-05T09:13:29-03:00"
}
],
"total": 1,
"total_pages": 1,
"current_page": 1,
"per_page": 20
}
}A listagem não traz encadeamento_detalhe (texto longo, pode concatenar várias mensagens) nem os campos do período (ano_pa, mes_pa, dia_pa, cno_afericao, num_proc_reclamatoria, chave_periodo, numero_recibo_alvo, ids_sistema_origem). Para o registro completo use GET /dctfweb/transmissao/{transmissao_id}.
Ordenação
sort_by aceita apenas estes valores; qualquer outro resulta em 422 (validação do FastAPI):
sort_by | Ordena por |
|---|---|
data_hora_inicio | Início da tentativa (default). |
data_hora_desfecho | Conclusão da tentativa. |
estado | Estado do desfecho. |
categoria | Categoria da declaração. |
competencia | Competência. |
modo | real / simulacao. |
cliente | customer.razao_social (faz JOIN com o cliente). |
Há desempate estável por id, na mesma direção do sort_dir - sem ele, valores repetidos deixariam a ordem indefinida entre páginas.
Escopo: dctfweb:read
GET /dctfweb/transmissao/{transmissao_id}
Detalhe de uma transmissão, com o registro completo - incluindo o período discriminado e o encadeamento_detalhe.
Parâmetros de rota
| Parâmetro | Tipo | Observações |
|---|---|---|
transmissao_id | int | ID do registro. Obrigatório. |
Response 200 OK
{
"status": "success",
"message": "Transmissão encontrada com sucesso!",
"data": {
"id": 118,
"id_cliente": 123,
"id_usuario": 7,
"atividade_id": 90514,
"categoria": "GERAL_MENSAL",
"competencia": "2026-04",
"chave_periodo": "GERAL_MENSAL|2026|04|-|-|-",
"ano_pa": "2026",
"mes_pa": "04",
"dia_pa": null,
"cno_afericao": null,
"num_proc_reclamatoria": null,
"modo": "real",
"estado": "transmitida",
"numero_recibo": "12.34.56.78-90",
"numero_recibo_alvo": null,
"codigo_serpro": null,
"classe_desfecho": null,
"mensagem": "Declaração transmitida com sucesso à Receita Federal.",
"ids_sistema_origem": "1,6,7",
"id_recibo": 5120,
"id_guia": 8830,
"encadeamento_estado": "ok",
"encadeamento_detalhe": null,
"data_hora_inicio": "2026-08-05T09:12:44-03:00",
"data_hora_desfecho": "2026-08-05T09:13:29-03:00"
}
}ID inexistente retorna 404 com "Transmissão de DCTFWeb com ID {id} não encontrada!".
Escopo: dctfweb:read
GET /dctfweb/situacao
Diz o que a Receita tem para o período - e, ao lado, o que nós registramos. É leitura pura: não muta nada.
Por que esta rota existe
Ela resolve o caso mais tenso do fluxo. Quando uma transmissão fica em indeterminado, a regra é não retransmitir - e, sem esta rota, a única saída seria conferir no e-CAC por fora. Aqui o operador descobre no próprio app se a declaração entrou.
Também serve para ver o guard antes de transmitir: se a situação é ATIVA, o POST vai responder ja_transmitida (a menos que se force a retransmissão).
Parâmetros de query
| Parâmetro | Tipo | Obrigatório | Observações |
|---|---|---|---|
cnpj | str | sim | CNPJ/CPF do contribuinte, com ou sem máscara. 11 a 20 caracteres. |
categoria | enum | sim | Rótulo da categoria (ex.: GERAL_MENSAL). Ver Catálogos. |
anoPA | str | sim | Ano de apuração, 4 dígitos: "2026". |
mesPA | str | condicional | Mês com zero à esquerda ("01"-"12"). Não se aplica ao 13º salário (41/51). |
diaPA | str | condicional | Dia do evento ("01"-"31"). Somente em ESPETACULO_DESPORTIVO (45). |
cnoAfericao | int | condicional | Número da obra (CNO), ≥ 0. Somente em AFERICAO (44). |
numProcReclamatoria | str | condicional | Nº do processo, até 60 caracteres. Somente em RECLAMATORIA_TRABALHISTA (46). |
As regras condicionais são as mesmas da transmissão - ver Campos condicionais por categoria. Violá-las devolve 422.
Diferente da transmissão, esta rota não valida se a categoria é transmissível: perguntar a situação de uma categoria que não se pode transmitir é legítimo.
Request
GET /api/v1/dctfweb/situacao?cnpj=27898481000150&categoria=GERAL_MENSAL&anoPA=2026&mesPA=04Response 200 OK
{
"status": "success",
"message": "Situação consultada com sucesso!",
"data": {
"id_cliente": 123,
"cnpj": "27898481000150",
"categoria": "GERAL_MENSAL",
"competencia": "2026-04",
"situacao": "ATIVA",
"numero_recibo": "12.34.56.78-90",
"codigo_serpro": null,
"mensagem": "A Receita tem uma declaração ATIVA para este período (recibo 12.34.56.78-90).",
"ultima_tentativa_local": {
"id": 118,
"estado": "transmitida",
"codigo_serpro": null,
"numero_recibo": "12.34.56.78-90",
"data_hora_inicio": "2026-08-05T09:12:44-03:00"
}
}
}Os 4 estados de situacao
Não existe serviço de "consultar situação" no Integra Contador. O vocabulário é derivado do único sinal disponível, o desfecho do CONSRECIBO32:
situacao | Derivado de | Significado |
|---|---|---|
ATIVA | A consulta respondeu com sucesso | Há declaração entregue no período. numero_recibo vem quando a Receita o informa. |
EM_ANDAMENTO | Código MG10 | A declaração mais recente do período ainda não foi transmitida. |
SEM_DECLARACAO | Código RELAT00 | Não há declaração ativa no período. É a única resposta que libera uma transmissão. |
INDETERMINADO | Qualquer outro desfecho | Não deu para concluir (timeout, indisponibilidade, código não mapeado). |
`INDETERMINADO` nunca significa "não transmitida"
A derivação é fail-closed: timeout, código não mapeado ou indisponibilidade nunca viram SEM_DECLARACAO. O estado negativo não é inferido de falha - SEM_DECLARACAO só é afirmado quando a Receita o diz explicitamente (RELAT00).
Um falso "não há declaração" faria o operador retransmitir algo já entregue, que é exatamente o dano que este domínio existe para evitar. Diante de INDETERMINADO: tente de novo em instantes, ou confira no portal da DCTFWeb.
Não existe campo `pode_transmitir` - de propósito
Não há resposta binária honesta. SEM_DECLARACAO libera; ATIVA normalmente barra, mas é exatamente o estado de quem reabriu e reencerrou a apuração e precisa retransmitir; e INDETERMINADO não autoriza nada. Um booleano teria de escolher uma dessas leituras e mentiria nas outras.
A decisão fica com quem tem o contexto, informada por situacao + mensagem + ultima_tentativa_local. É a divergência entre o que a Receita tem e o que nós registramos que explica o caso "mandei e não sei se entrou".
Escopo: dctfweb:read
Idempotência: por que uma transmissão repetida não duplica
A proteção é feita em quatro camadas, porque nenhuma sozinha basta:
| # | Camada | Contra o quê |
|---|---|---|
| 1 | Lock distribuído por (cliente, período) | Dois cliques simultâneos. É o caso que nenhuma consulta pega - as duas consultariam antes de qualquer uma transmitir. |
| 2 | Estado persistido (write-ahead) | Processo que morre no meio: o registro nasce como indeterminado antes do /Declarar, então o que fica no banco é a verdade, não a ausência de rastro. |
| 3 | CONSRECIBO32 (guard remoto) | Declaração entregue por fora (e-CAC, outra ferramenta) e não registrada aqui. |
| 4 | TRANS11 (palavra final do fisco) | Tudo o mais. É o próprio /Declarar dizendo "já foi transmitida". |
`TRANS11` é sucesso idempotente, não erro
Quando o Serpro devolve o código TRANS11 ("a declaração já foi transmitida"), a API não trata como falha: a operação já estava no estado desejado. O desfecho é 200 OK com estado: "ja_transmitida", classe_desfecho: "idempotente" e o fluxo segue para o encadeamento - recibo e guia são buscados normalmente.
Isso inclui o TRANS11 que chega escondido num HTTP 200: o Integra Contador tem envelope próprio e pode responder 200 com mensagens: [{"codigo": "[Erro-DCTFWEB-TRANS11]"}]. A API inspeciona o corpo, não só o status.
Duas observações sobre o guard, porque afetam o que você vê na resposta:
- A decisão vem do desfecho da chamada, não do número do recibo. O
CONSRECIBO32só responde para declaração ATIVA; se ele respondeu, há declaração entregue - mesmo que a extração donumeroReciboEntregadevolvanull. Por issonumero_recibopode vir nulo em um resultadoja_transmitidaou em umasituacao: "ATIVA": isso degrada a informação exibida, nunca a decisão. - Guard remoto inconclusivo não bloqueia. Se o
CONSRECIBO32falhar por instabilidade (código diferente deMG10/RELAT00), a transmissão segue - as outras três camadas continuam de pé. Bloquear ali impediria uma entrega devida por causa de uma consulta que nem é a operação pedida.
Não há índice único no banco por (cliente, categoria, período): retransmitir é caso de uso legítimo, e um índice único bloquearia exatamente o cenário que o domínio existe para atender.
A chave que identifica o que se declara
O guard não compara "cliente + competência + categoria" - isso não identifica uma declaração. Duas aferições de obras diferentes, ou dois espetáculos em dias diferentes do mesmo mês, são declarações distintas. A chave inclui os discriminadores da categoria:
<categoria>|<anoPA>|<mesPA>|<diaPA>|<cnoAfericao>|<numProcReclamatoria>Campos ausentes viram - (posicional, nunca omitido - assim 2026|04|- não colide com 2026|-|04). Exemplo de uma mensal: GERAL_MENSAL|2026|04|-|-|-.
O estado INDETERMINADO e a regra de consumo
O /Declarar nunca é repetido automaticamente pela API. Um retry por timeout numa transmissão é exatamente como se cria uma declaração duplicada: o cliente não recebeu a resposta, mas a Receita pode ter processado o envio - e o segundo envio chega como uma nova declaração, não como a mesma.
Por isso, em /Declarar, timeout, erro de conexão ou 5xx sem veredito no corpo não propagam "falhou": produzem a classe indeterminado, que é um terceiro estado, diferente de sucesso e de falha.
Regra de consumo em `indeterminado` / HTTP 504
- NÃO retransmita - nem manualmente, nem com retry automático do seu cliente HTTP. Configure o cliente para não repetir
POST /dctfweb/transmissao/. - Chame
GET /dctfweb/situacaocom o mesmo período.ATIVA⇒ entrou;SEM_DECLARACAO⇒ não entrou;INDETERMINADO⇒ ainda não dá para concluir - espere e tente a consulta de novo. - Se preferir, repita o mesmo
POST: a API detecta o registro anterior emindeterminadoe consulta o recibo antes de qualquer envio. Se a declaração estiver ATIVA, ela responde200comja_transmitidae a mensagem "A transmissão anterior deste período havia ficado sem confirmação, mas a consulta de recibo mostrou que a declaração ESTÁ entregue. Nada foi reenviado." - Se a consulta não esclarecer, a API responde
409e recusa -forcar_retransmissaonão libera este caso. O corpo traz{"message": {"estado": "indeterminado", "codigo": "...", "mensagem": "...", "transmissao_id": 118}}.
Quando o corpo de um 5xx traz um código reconhecido (TRANS11, TRANS01...), a Receita deu um veredito explícito - aí o desfecho é conhecido e vale o erro classificado, não o indeterminado.
Erros do Serpro: classe e status HTTP
A API não repassa o status HTTP do Serpro nem o corpo cru da resposta. Cada recusa é classificada, traduzida para pt-BR e recebe um status derivado da classe:
Classe (classe_desfecho) | HTTP | Quando | Retry adianta? |
|---|---|---|---|
sucesso_com_aviso | 200 | Aceito com ressalva (ex.: TRANS14, fora do prazo → MAED). | - |
idempotente | 409 | TRANS11. Na transmissão é interceptado e vira 200 (ver acima). | Não |
negocio | 422 | Estado da declaração, prazo, ausência de débito. Acionável pelo contador. | Não |
assinatura | 500 | Defeito nosso no XML/assinatura. Acionável pela equipe técnica. | Não |
transitorio | 502 | Falha momentânea do parceiro (5xx interno, 429, rede). | Sim, com backoff |
indeterminado | 504 | /Declarar sem veredito. Exclusivo de escrita. | Nunca |
desconhecido | 502 | Código não mapeado. Tratado como permanente (fail-closed). | Não |
O corpo do erro (sob a chave message, ver Envelope) tem esta forma:
{
"message": {
"classe": "negocio",
"codigo": "TRANS01",
"mensagem": "A declaração não está em estado que permita transmissão. Verifique a situação do período no portal da DCTFWeb antes de tentar de novo.",
"acionamento": "/Declarar",
"codigos": [{ "codigo": "TRANS01", "tipo": "Erro", "sistema": "DCTFWEB" }]
}
}mensagem é sempre texto nosso, redigido para o contador saber o que fazer - nunca o texto do Serpro. O codigo é o identificador curto e estável do parceiro, útil para diagnóstico e correlação com o log.
Códigos catalogados
| Código | Classe | Leitura |
|---|---|---|
TRANS11 | idempotente | Já havia sido transmitida. Não é erro. |
TRANS14 | sucesso_com_aviso | Transmitida fora do prazo: a Receita gerará multa por atraso (MAED). A transmissão foi aceita. |
TRANS01 | negocio | A declaração não está em estado que permita transmissão. |
TRANS13 | negocio | Período só admite consulta - não aceita mais transmissão. |
TRANS02 | assinatura | Assinatura fora do padrão (deve ter exatamente uma referência assinada). |
TRANS04 | assinatura | XML enviado não confere com o gerado pela Receita (hash divergente). |
TRANS09 | assinatura | Assinatura digital inválida. |
TRANS17 | assinatura | Assinatura posicionada no lugar errado do XML. |
TRANS21 | assinatura | XML assinado em base64 não foi enviado. |
MG10 | negocio | Existe declaração mais recente em andamento: informe numeroReciboEntrega. |
MG12 / MG19 / MG21 | transitorio | Falha interna momentânea da Receita. |
RELAT00 | negocio | Não há declaração ativa para o período. |
GUIA03 | negocio | Não há débitos com saldo a pagar para emissão da guia. |
Um TRANS14 numa transmissão bem-sucedida não vira erro: ele entra como mensagem do resultado (com classe_desfecho: "sucesso_com_aviso"), porque é a informação mais importante daquela transmissão para o contador.
Demais erros da API
| Status | Quando |
|---|---|
403 | Perfil sem permissão; API Key em rota de transmissão/simulação; CNPJ que não é de cliente do escritório. |
404 | GET /dctfweb/transmissao/{id} com ID inexistente. |
409 | Já existe transmissão em andamento para o mesmo cliente e período (lock); ou indeterminado não resolvido. |
422 | Corpo/query inválidos; categoria não transmissível; regras condicionais por categoria violadas; MG10 sem numeroReciboEntrega; XML da declaração ausente na Receita; XML não assinável. |
500 | Certificado A1 indisponível para assinar ("Certificado digital indisponível para assinar a DCTFWeb.") ou erro de assinatura reportado pelo Serpro. |
503 | Serviço de lock indisponível. A transmissão é recusada em vez de seguir sem exclusividade - sem lock, dois cliques viram duas declarações. |
504 | /Declarar sem veredito → estado indeterminado. |
Dois 422 merecem destaque, porque acontecem antes de qualquer envio:
MG10sem recibo alvo - há uma declaração mais recente em andamento e a Receita precisa saber qual é o alvo. O corpo é{"message": {"codigo": "MG10", "mensagem": "..."}}. InformenumeroReciboEntregae repita.- XML ausente -
"A Receita não devolveu o XML da declaração para este período (campo XMLStringBase64 ausente). Confirme que a apuração está encerrada antes de transmitir."Quase sempre significa apuração não encerrada.
Campos condicionais por categoria
As regras são as mesmas de todo o domínio DCTFWeb e vêm do mesmo catálogo - não há uma segunda tabela que possa divergir. A referência completa (código, rótulo, exigeMesPA, camposExtras) está em DCTFWeb: Catálogos. Em resumo:
mesPA- obrigatório, exceto nas categorias de 13º salário (41GERAL_13o_SALARIOe51PF_13o_SALARIO), que são anuais. Nelas, informarmesPAé recusado com"'mesPA' não se aplica à categoria {rótulo} (declaração anual de 13º salário)."diaPA- somente emESPETACULO_DESPORTIVO(45), onde é obrigatório.numProcReclamatoria- somente emRECLAMATORIA_TRABALHISTA(46), onde é obrigatório.cnoAfericao- somente emAFERICAO(44), onde é obrigatório.
Informar um desses campos fora da sua categoria devolve 422 com "'{campo}' só se aplica à categoria {rótulo}."; omiti-lo na categoria dona devolve "'{campo}' é obrigatório na categoria {rótulo}."
Nas categorias anuais, a competencia derivada é "AAAA" (e não "AAAA-MM"): nenhum mês sintético é inventado. Isso afeta o filtro competencia da listagem e o campo competencia de todas as respostas.
`cnoAfericao` na transmissão é uma aposta ainda não validada
O campo cnoAfericao está documentado pela Serpro nos serviços CONSRECIBO32, CONSDECCOMPLETA33, GERARGUIA31 e CONSXMLDECLARACAO38 - mas não consta na página oficial do TRANSDECLARACAO310. Mesmo assim, a API o envia na transmissão da categoria 44, e a categoria segue com habilitadaTransmissao: true.
Isso é uma decisão deliberada, não um descuido: a leitura é que a ausência na página de transmissão é omissão da documentação, não proibição - não faria sentido a API deixar consultar e emitir guia de uma aferição e não deixar transmiti-la, e sem o CNO não há como identificar a obra. A alternativa seria bloquear a tentativa com um 422 nosso baseado num palpite.
O que isso significa para quem integra: transmitir uma aferição de obra pode ser recusado pela Receita por campo não reconhecido (códigos do tipo EntradaIncorreta, MG07, MG20). Se isso acontecer, não é bug do seu cliente - avise a equipe técnica. Enquanto este aviso estiver aqui, ninguém validou o comportamento contra a Receita. As outras seis categorias não têm essa ressalva.
Schemas
DctfwebTransmitirRequest
Corpo de POST /dctfweb/transmissao/ e de POST /dctfweb/transmissao/simular.
| Campo | Tipo | Obrigatório | Observações |
|---|---|---|---|
cnpj | str (11-20) | sim | CNPJ/CPF do contribuinte, com ou sem máscara. Precisa pertencer a um cliente do escritório. |
categoria | enum | sim | Rótulo da categoria (ex.: GERAL_MENSAL). Categoria não transmissível → 422. |
anoPA | str (^\d{4}$) | sim | Ano de apuração: "2026". String, não inteiro. |
mesPA | str | condicional | "01"-"12", com zero à esquerda. Ver campos condicionais. |
diaPA | str | condicional | "01"-"31". Somente em ESPETACULO_DESPORTIVO. |
cnoAfericao | int (≥ 0) | condicional | Número da obra. Numérico, ao contrário de ano/mês. Somente em AFERICAO. |
numProcReclamatoria | str (≤ 60) | condicional | Nº do processo. Somente em RECLAMATORIA_TRABALHISTA. |
numeroReciboEntrega | str (≤ 60) | não | Recibo da declaração alvo. Exigido pela Receita quando há declaração mais recente em andamento (MG10). |
idsSistemaOrigem | List[int] | não | Sistemas de origem da guia encadeada. Omitir ≠ lista vazia - ver abaixo. |
forcar_retransmissao | bool | não | Default false. Segue mesmo havendo declaração ATIVA. Não libera o estado indeterminado. |
Note a convenção mista de nomes, que é deliberada: campos que vão crus para o Integra Contador mantêm o nome do contrato em camelCase (anoPA, numeroReciboEntrega, idsSistemaOrigem); campos nossos usam snake_case (cnpj, forcar_retransmissao).
Campo desconhecido é recusado
Ao contrário de outros schemas do domínio Serpro, este usa extra="forbid": qualquer campo não listado acima resulta em 422. Numa requisição de transmissão, um campo desconhecido quase sempre é erro de digitação - e aceitá-lo em silêncio faria a declaração sair com o período errado sem ninguém perceber.
`idsSistemaOrigem`: omitir ≠ lista vazia
- Campo omitido (ausente do corpo) → a guia encadeada sai com todas as receitas. É um estado legítimo e é o default.
- Lista vazia (
[]) → erro422. Uma guia de nenhuma origem não existe.
Códigos aceitos: 1 (eSocial), 5 (Sero), 6 (Reinf CP), 7 (Reinf RET), 8 (MIT), sem repetição. Detalhes e presets em DCTFWeb: Catálogos.
DctfwebTransmissaoResultado
data das duas rotas POST.
| Campo | Tipo | Observações |
|---|---|---|
id | int | null | ID do registro persistido no histórico. |
id_cliente | int | Cliente resolvido a partir do cnpj. |
cnpj | str | Somente dígitos (a máscara enviada é descartada). |
categoria | str | Rótulo da categoria. |
competencia | str | AAAA-MM; AAAA nas categorias anuais. |
modo | str | real | simulacao. |
estado | str | simulada | transmitida | ja_transmitida | indeterminado | falha. |
mensagem | str | Texto em pt-BR, acionável. |
codigo_serpro | str | null | Código do parceiro, ou marcador interno (DRY-RUN, GUARD-LOCAL, GUARD-REMOTO, INDETERMINADO-RESOLVIDO). |
classe_desfecho | str | null | Classe do desfecho - ver a tabela de classes. |
numero_recibo | str | null | Recibo de entrega, quando obtido. Pode ser null mesmo em sucesso. |
encadeamento | EncadeamentoOut | null | Desfecho do recibo + guia. null na simulação. |
payload_simulado | PayloadSimulado | null | Só no dry-run. |
EncadeamentoOut
| Campo | Tipo | Observações |
|---|---|---|
estado | str | nao_executado | ok | parcial | falha. |
numero_recibo | str | null | Recibo obtido no CONSRECIBO32. |
id_recibo | int | null | ID do registro criado em Recibos. |
id_guia | int | null | ID do registro criado em Guias. |
detalhe | str | null | Falhas acumuladas, separadas por |. null quando não houve nenhuma. |
ok exige recibo e guia sem nenhuma ressalva; falha é recibo com erro e guia não gravada; qualquer meio-termo é parcial.
PayloadSimulado
Só existe no dry-run.
| Campo | Tipo | Observações |
|---|---|---|
idSistema | str | DCTFWEB. |
idServico | str | TRANSDECLARACAO310. |
acionamento | str | Endpoint que seria chamado: /Declarar. |
contribuinte | object | { tipo, numero } - 1 = PF (CPF), 2 = PJ (CNPJ), derivado do comprimento. |
dados | object | Payload completo, exceto xmlAssinadoBase64. |
xml_assinado | Base64Resumo | Resumo do XML já assinado. |
xml_original | Base64Resumo | Resumo do que veio do CONSXMLDECLARACAO38. |
Base64Resumo
| Campo | Tipo | Observações |
|---|---|---|
tamanho | int | Comprimento do base64, em caracteres. |
sha256 | str | SHA-256 do base64, em hex - os 16 primeiros caracteres. Para comparar execuções. |
preview | str | Primeiros 64 caracteres, para conferência visual. |
O base64 completo nunca é devolvido.
DctfwebSituacaoOut
data de GET /dctfweb/situacao.
| Campo | Tipo | Observações |
|---|---|---|
id_cliente | int | Cliente resolvido a partir do cnpj. |
cnpj | str | Somente dígitos. |
categoria | str | Rótulo da categoria. |
competencia | str | AAAA-MM; AAAA nas anuais. |
situacao | str | ATIVA | EM_ANDAMENTO | SEM_DECLARACAO | INDETERMINADO. |
numero_recibo | str | null | Recibo de entrega, quando ATIVA. Pode vir nulo mesmo em ATIVA. |
codigo_serpro | str | null | MG10, RELAT00, etc. null quando a consulta respondeu com sucesso. |
mensagem | str | Texto em pt-BR explicando a situação. |
ultima_tentativa_local | UltimaTentativaLocal | null | A última tentativa real registrada por nós para o mesmo período. |
UltimaTentativaLocal
| Campo | Tipo | Observações |
|---|---|---|
id | int | ID no histórico de transmissões. |
estado | str | Estado daquela tentativa. |
codigo_serpro | str | null | Código registrado. |
numero_recibo | str | null | Recibo registrado. |
data_hora_inicio | datetime | null | Início da tentativa. |
Simulações não entram aqui: um dry-run não prova nada sobre o que a Receita registrou.
DctfwebTransmissaoListItem
Item de GET /dctfweb/transmissao/.
| Campo | Tipo | Observações |
|---|---|---|
id | int | - |
id_cliente | int | - |
id_usuario | int | null | Autoria. Fica null se o usuário for excluído. |
atividade_id | int | null | Atividade registrada na timeline do cliente. |
categoria | str | - |
competencia | str | - |
modo | str | real | simulacao. |
estado | str | - |
numero_recibo | str | null | - |
codigo_serpro | str | null | - |
classe_desfecho | str | null | - |
mensagem | str | null | Curta e acionável - é o que serve numa lista de falhas. |
id_recibo | int | null | - |
id_guia | int | null | - |
encadeamento_estado | str | null | - |
data_hora_inicio | datetime | null | - |
data_hora_desfecho | datetime | null | null enquanto a tentativa não fechou. |
DctfwebTransmissaoRead
data de GET /dctfweb/transmissao/{transmissao_id}. Tem todos os campos do item da listagem, mais:
| Campo | Tipo | Observações |
|---|---|---|
chave_periodo | str | Chave de idempotência do período - ver a chave que identifica o que se declara. |
ano_pa | str | - |
mes_pa | str | null | - |
dia_pa | str | null | - |
cno_afericao | str | null | Gravado como texto, ainda que trafegue como número na requisição. |
num_proc_reclamatoria | str | null | - |
numero_recibo_alvo | str | null | O numeroReciboEntrega enviado no payload. Separado de numero_recibo, que é resultado. |
ids_sistema_origem | str | null | CSV (ex.: "1,6,7"). null significa campo omitido = guia com todas as receitas. |
encadeamento_detalhe | str | null | Texto longo com as falhas do encadeamento. |
PaginatedDctfwebTransmissoes
| Campo | Tipo | Observações |
|---|---|---|
items | List[object] | DctfwebTransmissaoListItem. |
total | int | Total de registros que casam com os filtros. |
total_pages | int | 1 quando per_page é omitido. |
current_page | int | Página atual. |
per_page | int | null | null quando o cliente não paginou. |
Notas
- As duas rotas
POSTnão aceitam API Key. É a única superfície da DCTFWeb com essa restrição. POST /dctfweb/transmissao/responde200, não201- inclusive quando cria um registro no histórico. O recurso criado é a tentativa, e o que interessa ao consumidor é oestado.- Todo registro é uma tentativa, inclusive as barradas pelo guard (que nunca chamaram o
/Declarar) e as simuladas. Quem tentou e foi barrado faz parte da auditoria. - A autoria sobrevive à exclusão do usuário:
id_usuarioviranull, o registro permanece. O mesmo vale para recibo, guia e atividade referenciados. - Não há rota de cancelamento, exclusão ou edição de transmissão. O histórico é somente-leitura pela API.
- Estas rotas não substituem nada:
POST /integra-contador/dctf/transmitirePOST /integra-contador/dctf/consultar-xmlforam removidas (validavam contra schemas incorretos e nunca operaram) e não ganharam alias - respondem404. A superfície nova nasceu sob/dctfwebe não tem caminho legado.