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/transmissao

A 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/ e POST /dctfweb/transmissao/simular.
  • dctfweb:read - GET /dctfweb/transmissao/, GET /dctfweb/transmissao/{transmissao_id} e GET /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:

  1. Escopo dctfweb:write - derivado do path, como em todo o resto da API.
  2. Perfil administrador ou diretoria. Um token com o escopo correto mas perfil operacional é recusado com 403.
  3. 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 erroCorpo
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:

#EtapaO que acontece
1CONSXMLDECLARACAO38 (acionamento /Consultar)Busca na Receita o XML da declaração do período (XMLStringBase64).
2Assinatura local (XMLDSig)Assina o XML com o certificado A1 do escritório. Roda fora do event loop.
3TRANSDECLARACAO310 (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:

estadoHTTPSignificadoO que fazer
transmitida200O /Declarar aceitou nesta execução.Nada. Recibo e guia vêm em encadeamento.
ja_transmitida200A declaração já estava entregue - via guard local/remoto ou via TRANS11. Não é erro.Nada. É o estado desejado.
indeterminado504O /Declarar não respondeu. Pode ter entrado.NÃO retransmita. Consulte GET /dctfweb/situacao.
falha422, 500, 502Recusa 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âmetroTipoDefaultObservações
id_clienteint-Filtra por cliente.
id_usuarioint-Filtra por autoria (quem disparou).
categoriastr-Ex.: GERAL_MENSAL. Case-insensitive, ignora espaços nas pontas.
competenciastr-AAAA-MM; AAAA nas categorias de 13º salário. Comparação exata.
estadostr-simulada | transmitida | ja_transmitida | indeterminado | falha. Case-insensitive.
modostr-real | simulacao. Case-insensitive.
start_datedatetime-data_hora_inicio >= start_date.
end_datedatetime-data_hora_inicio <= end_date.
sort_bystr (whitelist)data_hora_inicioWhitelist na seção Ordenação, abaixo. Valor fora dela → 422.
sort_dirasc | descdescValor fora da whitelist → 422.
pageint (≥ 1)1Página, 1-based.
per_pageint (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=20

Response 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_byOrdena por
data_hora_inicioInício da tentativa (default).
data_hora_desfechoConclusão da tentativa.
estadoEstado do desfecho.
categoriaCategoria da declaração.
competenciaCompetência.
modoreal / simulacao.
clientecustomer.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âmetroTipoObservações
transmissao_idintID 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âmetroTipoObrigatórioObservações
cnpjstrsimCNPJ/CPF do contribuinte, com ou sem máscara. 11 a 20 caracteres.
categoriaenumsimRótulo da categoria (ex.: GERAL_MENSAL). Ver Catálogos.
anoPAstrsimAno de apuração, 4 dígitos: "2026".
mesPAstrcondicionalMês com zero à esquerda ("01"-"12"). Não se aplica ao 13º salário (41/51).
diaPAstrcondicionalDia do evento ("01"-"31"). Somente em ESPETACULO_DESPORTIVO (45).
cnoAfericaointcondicionalNúmero da obra (CNO), ≥ 0. Somente em AFERICAO (44).
numProcReclamatoriastrcondicionalNº 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=04

Response 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:

situacaoDerivado deSignificado
ATIVAA consulta respondeu com sucessoHá declaração entregue no período. numero_recibo vem quando a Receita o informa.
EM_ANDAMENTOCódigo MG10A declaração mais recente do período ainda não foi transmitida.
SEM_DECLARACAOCódigo RELAT00Não há declaração ativa no período. É a única resposta que libera uma transmissão.
INDETERMINADOQualquer outro desfechoNã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:

#CamadaContra o quê
1Lock distribuído por (cliente, período)Dois cliques simultâneos. É o caso que nenhuma consulta pega - as duas consultariam antes de qualquer uma transmitir.
2Estado 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.
3CONSRECIBO32 (guard remoto)Declaração entregue por fora (e-CAC, outra ferramenta) e não registrada aqui.
4TRANS11 (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 CONSRECIBO32 só responde para declaração ATIVA; se ele respondeu, há declaração entregue - mesmo que a extração do numeroReciboEntrega devolva null. Por isso numero_recibo pode vir nulo em um resultado ja_transmitida ou em uma situacao: "ATIVA": isso degrada a informação exibida, nunca a decisão.
  • Guard remoto inconclusivo não bloqueia. Se o CONSRECIBO32 falhar por instabilidade (código diferente de MG10/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

  1. NÃO retransmita - nem manualmente, nem com retry automático do seu cliente HTTP. Configure o cliente para não repetir POST /dctfweb/transmissao/.
  2. Chame GET /dctfweb/situacao com 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.
  3. Se preferir, repita o mesmo POST: a API detecta o registro anterior em indeterminado e consulta o recibo antes de qualquer envio. Se a declaração estiver ATIVA, ela responde 200 com ja_transmitida e 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."
  4. Se a consulta não esclarecer, a API responde 409 e recusa - forcar_retransmissao nã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)HTTPQuandoRetry adianta?
sucesso_com_aviso200Aceito com ressalva (ex.: TRANS14, fora do prazo → MAED).-
idempotente409TRANS11. Na transmissão é interceptado e vira 200 (ver acima).Não
negocio422Estado da declaração, prazo, ausência de débito. Acionável pelo contador.Não
assinatura500Defeito nosso no XML/assinatura. Acionável pela equipe técnica.Não
transitorio502Falha momentânea do parceiro (5xx interno, 429, rede).Sim, com backoff
indeterminado504/Declarar sem veredito. Exclusivo de escrita.Nunca
desconhecido502Có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ódigoClasseLeitura
TRANS11idempotenteJá havia sido transmitida. Não é erro.
TRANS14sucesso_com_avisoTransmitida fora do prazo: a Receita gerará multa por atraso (MAED). A transmissão foi aceita.
TRANS01negocioA declaração não está em estado que permita transmissão.
TRANS13negocioPeríodo só admite consulta - não aceita mais transmissão.
TRANS02assinaturaAssinatura fora do padrão (deve ter exatamente uma referência assinada).
TRANS04assinaturaXML enviado não confere com o gerado pela Receita (hash divergente).
TRANS09assinaturaAssinatura digital inválida.
TRANS17assinaturaAssinatura posicionada no lugar errado do XML.
TRANS21assinaturaXML assinado em base64 não foi enviado.
MG10negocioExiste declaração mais recente em andamento: informe numeroReciboEntrega.
MG12 / MG19 / MG21transitorioFalha interna momentânea da Receita.
RELAT00negocioNão há declaração ativa para o período.
GUIA03negocioNã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

StatusQuando
403Perfil sem permissão; API Key em rota de transmissão/simulação; CNPJ que não é de cliente do escritório.
404GET /dctfweb/transmissao/{id} com ID inexistente.
409Já existe transmissão em andamento para o mesmo cliente e período (lock); ou indeterminado não resolvido.
422Corpo/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.
500Certificado A1 indisponível para assinar ("Certificado digital indisponível para assinar a DCTFWeb.") ou erro de assinatura reportado pelo Serpro.
503Serviç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:

  • MG10 sem recibo alvo - há uma declaração mais recente em andamento e a Receita precisa saber qual é o alvo. O corpo é {"message": {"codigo": "MG10", "mensagem": "..."}}. Informe numeroReciboEntrega e 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 (41 GERAL_13o_SALARIO e 51 PF_13o_SALARIO), que são anuais. Nelas, informar mesPA é recusado com "'mesPA' não se aplica à categoria {rótulo} (declaração anual de 13º salário)."
  • diaPA - somente em ESPETACULO_DESPORTIVO (45), onde é obrigatório.
  • numProcReclamatoria - somente em RECLAMATORIA_TRABALHISTA (46), onde é obrigatório.
  • cnoAfericao - somente em AFERICAO (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.

CampoTipoObrigatórioObservações
cnpjstr (11-20)simCNPJ/CPF do contribuinte, com ou sem máscara. Precisa pertencer a um cliente do escritório.
categoriaenumsimRótulo da categoria (ex.: GERAL_MENSAL). Categoria não transmissível → 422.
anoPAstr (^\d{4}$)simAno de apuração: "2026". String, não inteiro.
mesPAstrcondicional"01"-"12", com zero à esquerda. Ver campos condicionais.
diaPAstrcondicional"01"-"31". Somente em ESPETACULO_DESPORTIVO.
cnoAfericaoint (≥ 0)condicionalNúmero da obra. Numérico, ao contrário de ano/mês. Somente em AFERICAO.
numProcReclamatoriastr (≤ 60)condicionalNº do processo. Somente em RECLAMATORIA_TRABALHISTA.
numeroReciboEntregastr (≤ 60)nãoRecibo da declaração alvo. Exigido pela Receita quando há declaração mais recente em andamento (MG10).
idsSistemaOrigemList[int]nãoSistemas de origem da guia encadeada. Omitir ≠ lista vazia - ver abaixo.
forcar_retransmissaoboolnãoDefault 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 ([]) → erro 422. 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.

CampoTipoObservações
idint | nullID do registro persistido no histórico.
id_clienteintCliente resolvido a partir do cnpj.
cnpjstrSomente dígitos (a máscara enviada é descartada).
categoriastrRótulo da categoria.
competenciastrAAAA-MM; AAAA nas categorias anuais.
modostrreal | simulacao.
estadostrsimulada | transmitida | ja_transmitida | indeterminado | falha.
mensagemstrTexto em pt-BR, acionável.
codigo_serprostr | nullCódigo do parceiro, ou marcador interno (DRY-RUN, GUARD-LOCAL, GUARD-REMOTO, INDETERMINADO-RESOLVIDO).
classe_desfechostr | nullClasse do desfecho - ver a tabela de classes.
numero_recibostr | nullRecibo de entrega, quando obtido. Pode ser null mesmo em sucesso.
encadeamentoEncadeamentoOut | nullDesfecho do recibo + guia. null na simulação.
payload_simuladoPayloadSimulado | null no dry-run.

EncadeamentoOut

CampoTipoObservações
estadostrnao_executado | ok | parcial | falha.
numero_recibostr | nullRecibo obtido no CONSRECIBO32.
id_reciboint | nullID do registro criado em Recibos.
id_guiaint | nullID do registro criado em Guias.
detalhestr | nullFalhas 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.

CampoTipoObservações
idSistemastrDCTFWEB.
idServicostrTRANSDECLARACAO310.
acionamentostrEndpoint que seria chamado: /Declarar.
contribuinteobject{ tipo, numero } - 1 = PF (CPF), 2 = PJ (CNPJ), derivado do comprimento.
dadosobjectPayload completo, exceto xmlAssinadoBase64.
xml_assinadoBase64ResumoResumo do XML já assinado.
xml_originalBase64ResumoResumo do que veio do CONSXMLDECLARACAO38.

Base64Resumo

CampoTipoObservações
tamanhointComprimento do base64, em caracteres.
sha256strSHA-256 do base64, em hex - os 16 primeiros caracteres. Para comparar execuções.
previewstrPrimeiros 64 caracteres, para conferência visual.

O base64 completo nunca é devolvido.

DctfwebSituacaoOut

data de GET /dctfweb/situacao.

CampoTipoObservações
id_clienteintCliente resolvido a partir do cnpj.
cnpjstrSomente dígitos.
categoriastrRótulo da categoria.
competenciastrAAAA-MM; AAAA nas anuais.
situacaostrATIVA | EM_ANDAMENTO | SEM_DECLARACAO | INDETERMINADO.
numero_recibostr | nullRecibo de entrega, quando ATIVA. Pode vir nulo mesmo em ATIVA.
codigo_serprostr | nullMG10, RELAT00, etc. null quando a consulta respondeu com sucesso.
mensagemstrTexto em pt-BR explicando a situação.
ultima_tentativa_localUltimaTentativaLocal | nullA última tentativa real registrada por nós para o mesmo período.

UltimaTentativaLocal

CampoTipoObservações
idintID no histórico de transmissões.
estadostrEstado daquela tentativa.
codigo_serprostr | nullCódigo registrado.
numero_recibostr | nullRecibo registrado.
data_hora_iniciodatetime | nullIní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/.

CampoTipoObservações
idint-
id_clienteint-
id_usuarioint | nullAutoria. Fica null se o usuário for excluído.
atividade_idint | nullAtividade registrada na timeline do cliente.
categoriastr-
competenciastr-
modostrreal | simulacao.
estadostr-
numero_recibostr | null-
codigo_serprostr | null-
classe_desfechostr | null-
mensagemstr | nullCurta e acionável - é o que serve numa lista de falhas.
id_reciboint | null-
id_guiaint | null-
encadeamento_estadostr | null-
data_hora_iniciodatetime | null-
data_hora_desfechodatetime | nullnull enquanto a tentativa não fechou.

DctfwebTransmissaoRead

data de GET /dctfweb/transmissao/{transmissao_id}. Tem todos os campos do item da listagem, mais:

CampoTipoObservações
chave_periodostrChave de idempotência do período - ver a chave que identifica o que se declara.
ano_pastr-
mes_pastr | null-
dia_pastr | null-
cno_afericaostr | nullGravado como texto, ainda que trafegue como número na requisição.
num_proc_reclamatoriastr | null-
numero_recibo_alvostr | nullO numeroReciboEntrega enviado no payload. Separado de numero_recibo, que é resultado.
ids_sistema_origemstr | nullCSV (ex.: "1,6,7"). null significa campo omitido = guia com todas as receitas.
encadeamento_detalhestr | nullTexto longo com as falhas do encadeamento.

PaginatedDctfwebTransmissoes

CampoTipoObservações
itemsList[object]DctfwebTransmissaoListItem.
totalintTotal de registros que casam com os filtros.
total_pagesint1 quando per_page é omitido.
current_pageintPágina atual.
per_pageint | nullnull quando o cliente não paginou.

Notas

  • As duas rotas POST não aceitam API Key. É a única superfície da DCTFWeb com essa restrição.
  • POST /dctfweb/transmissao/ responde 200, não 201 - inclusive quando cria um registro no histórico. O recurso criado é a tentativa, e o que interessa ao consumidor é o estado.
  • 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_usuario vira null, 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/transmitir e POST /integra-contador/dctf/consultar-xml foram removidas (validavam contra schemas incorretos e nunca operaram) e não ganharam alias - respondem 404. A superfície nova nasceu sob /dctfweb e não tem caminho legado.