Nota Plena · API

Documentação da API

A Nota Plena expõe uma API completa para parceiros (ERPs, CRMs e plataformas verticais — Surfbook, sistemas de academia, escolas). Você pode criar empresas, subir certificados A1, emitir NFS-e programaticamente, ler emissões, gerar links públicos de visualização e sincronizar o status de cobrança das empresas que você opera.

Autenticação

Toda requisição usa chaves Bearer escopadas. Há dois tipos:

  • Chave de empresa (np_live_…) — só acessa a empresa que a emitiu. Disponível no plano Negócio; criada pelo dono da empresa em notaplena.com.br/settings → Integrações.
  • Chave de parceiro (np_live_…, sem vínculo a uma empresa) — pode criar e operar várias empresas em nome do parceiro. Emitida sob acordo B2B pela Nota Plena.

Envie a chave no header de toda requisição:

Authorization: Bearer np_sk_demo_xxxxxxxxxxxxxxxxxxxxxxxx
Escopo: uma chave de parceiro só lê/altera empresas que ela mesma criou. Acesso cruzado (empresa de outro parceiro) responde 404 — nunca 403 — para não revelar existência.

Limite de uso

Cada chave de parceiro — e cada chave de empresa nas rotas de emissão e cancelamento — é limitada a 120 requisições por minuto (janela deslizante). Excedeu? Resposta 429. Para volumes maiores combine previamente com o time NP.

URL base e ambientes

Produção:

https://api.notaplena.com.br

Todos os endpoints abaixo usam essa origem. Respostas são JSON, com corpo de erro no formato:

{ "detail": "Mensagem em português explicando o erro" }

Sandbox: empresas criadas por chaves de parceiro nascem em modo sandbox. Emissões em sandbox não tocam a prefeitura — devolvem um numero_nfse sintético (prefixo SANDBOX-) para você validar o fluxo do seu lado. Para virar produção use activate-production.

Fallback diagnóstico: https://nota-plena-api.fly.dev aponta direto para o backend no Fly e pode ser usada apenas para diagnóstico (testar conectividade quando o DNS de api.notaplena.com.br estiver em dúvida). Não use em produção — a URL canônica é a primeira.

Empresas (tenants)

Endpoints para criar e gerenciar as empresas (CNPJs) que o parceiro opera na Nota Plena.

Criar empresa

POST/api/v1/integrations/tenants

Cria uma nova empresa em modo sandbox. O CNPJ precisa ser único — duplicado responde 409.

curl -X POST https://api.notaplena.com.br/api/v1/integrations/tenants \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "cnpj": "65851791000109",
    "razao_social": "Escola de Surfe Ana Maria LTDA",
    "nome_fantasia": "Surf da Ana",
    "ibge_city_code": "3550308",
    "inscricao_municipal": "02595273",
    "simples_ativo": true,
    "is_mei": false,
    "anexo": "III",
    "contact_email": "ana@exemplo.com.br"
  }'

Resposta 201

{
  "id": "9c1fa402-3c29-4bbb-9210-baf4cc2a4231",
  "cnpj": "65851791000109",
  "razao_social": "Escola de Surfe Ana Maria LTDA",
  "nome_fantasia": "Surf da Ana",
  "ibge_city_code": "3550308",
  "inscricao_municipal": "02595273",
  "mode": "sandbox",
  "status": "active",
  "simples_ativo": true,
  "is_mei": false,
  "anexo": "III"
}

simples_ativo é obrigatório (true = Simples Nacional, false = Lucro Presumido/Real), exceto quando is_mei=true. Sem ele a resposta é 422 com REGIME_REQUIRED e nenhuma empresa é criada — o regime decide se a nota sai pela prefeitura ou pelo Emissor Nacional.

Campos opcionais no request: nome_fantasia, inscricao_municipal, is_mei, anexo ("III" ou "V"), contact_email. Podem ser ajustados depois em PUT /config.

Listar empresas

GET/api/v1/integrations/tenants

Retorna todas as empresas criadas por esta chave de parceiro, da mais nova para a mais antiga. Use para reconciliar a sua base de empresas após uma falha de processo.

curl https://api.notaplena.com.br/api/v1/integrations/tenants \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

Ler uma empresa

GET/api/v1/integrations/tenants/{tenant_id}

Estado atual de uma única empresa. Se a empresa não foi criada por esta chave, responde 404.

curl https://api.notaplena.com.br/api/v1/integrations/tenants/9c1fa402-... \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

Atualizar configuração

PUT/api/v1/integrations/tenants/{tenant_id}/config

Atualiza campos fiscais / cadastrais. Só envie os campos que mudaram — o restante permanece como está.

curl -X PUT https://api.notaplena.com.br/api/v1/integrations/tenants/9c1fa402-.../config \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "inscricao_municipal": "02595273",
    "anexo": "III",
    "nome_fantasia": "Surf da Ana"
  }'

Campos aceitos: inscricao_municipal, simples_ativo, is_mei, anexo, nome_fantasia, razao_social, default_codigo_servico, contact_email. Os dois últimos são aceitos para compatibilidade futura, mas ainda não são persistidos (não confie neles para leitura).

Ativar produção

POST/api/v1/integrations/tenants/{tenant_id}/activate-production

Vira a empresa de sandbox para production. Para ser aprovada, a empresa precisa ter:

  • certificado A1 válido e não expirado;
  • inscricao_municipal preenchida;
  • regime fiscal consistente (se is_mei=true, então simples_ativo e anexo em "III" ou "V");
  • pelo menos uma emissão sandbox bem-sucedida — prova que o shape dos dados está correto.

Se algo faltar, resposta 409 listando os itens pendentes. Em caso de sucesso, retorna a empresa com mode: "production".

Campos extra no banco: a empresa também guarda partner_referrer (slug do parceiro que a criou) e partner_active_status (true/false/null) usados pelo gate de emissão. Esses dois não estão expostos no PartnerTenantOut ainda — para consultá-los use GET /active-status.

Certificados A1

POST/api/v1/integrations/tenants/{tenant_id}/certs

Sobe um certificado digital A1 (.pfx) e a senha para uma empresa. Envio via multipart/form-data. Ao subir um novo, o anterior é automaticamente marcado como inativo (rotação transparente).

curl -X POST https://api.notaplena.com.br/api/v1/integrations/tenants/9c1fa402-.../certs \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -F "pfx_file=@./certificado.pfx" \
  -F "password=senha-do-pfx"

Resposta 201

{
  "id": "f4b2-...-c128",
  "fingerprint_sha256": "ab12cd34...",
  "not_before": "2026-01-12T00:00:00+00:00",
  "not_after":  "2027-01-12T23:59:59+00:00",
  "is_active":  true
}
LGPD / boas práticas: em vez de trafegar o .pfx e a senha pelos seus servidores, prefira a página de onboarding hospedada pela Nota Plena — o usuário final sobe o certificado direto no NP, sem passar pela sua infra. Reduz escopo LGPD e elimina o risco de credencial criptográfica vazar pelo seu pipeline. Solicite o link com o time NP.

Webhook de expiração

Quando o tempo de vida do certificado cruza pela primeira vez a barreira de 30 dias para expirar, disparamos o evento cert.expiring_30d nos webhooks subscritos pela sua chave. A entrega é idempotente (uma vez só por certificado). Veja Webhooks.

Emitir NFS-e (programático)

POST/api/v1/integrations/nfse

Emite uma NFS-e em nome de uma empresa que o parceiro opera. Em sandbox a resposta é sintética; em production roda o adapter da prefeitura. A primeira emissão sandbox bem-sucedida desbloqueia activate-production.

curl -X POST https://api.notaplena.com.br/api/v1/integrations/nfse \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: surfbook-payment-93271" \
  -d '{
    "np_tenant_id": "9c1fa402-3c29-4bbb-9210-baf4cc2a4231",
    "valor_servicos": "650.00",
    "discriminacao": "Consultoria — pacote 4 sessões",
    "codigo_servico": "10.05",
    "aliquota_iss": "0.02",
    "iss_retido": false,
    "deducoes": "0",
    "tomador_nome": "Ana Maria Pereira",
    "tomador_documento": "12345678901",
    "tomador_email": "ana@exemplo.com.br",
    "tomador_endereco": "Rua dos Surfistas, 100",
    "tomador_ibge_code": "3550308",
    "tomador_cep": "04023-062"
  }'

Resposta 200

{
  "emission_id": "9c1fa402-3c29-4bbb-9210-baf4cc2a4231",
  "status": "success",
  "numero_nfse": "1023",
  "codigo_verificacao": "NMTBGWKL",
  "error_message": null
}

status: sandbox (modo teste, numero sintético), success (emitido na prefeitura), error (recusado — error_message e o objeto error trazem o motivo) ou pending (em processamento assíncrono; a nota ainda não tem número — aguarde o webhook nfse.emitted/nfse.failed, não reenvie).

Uma recusa da prefeitura (dados fiscais errados, etc.) chega como HTTP 200 com status: "error" — não é erro HTTP. Prefira o objeto estruturado error ({ code, field, hint }) para ramificar no seu código; error_message é o texto cru da prefeitura. Como tratar cada caso está em Erros e retentativas.

Idempotência

O header Idempotency-Key (até 80 caracteres) é obrigatório na emissão — sem ele a resposta é 422 IDEMPOTENCY_KEY_REQUIRED e nada é emitido. É ele que torna a chamada segura para retries. Reenviar a mesma chave para a mesma empresa devolve a emissão original sem disparar nova ida à prefeitura. Use uma referência única do seu sistema (ex.: ID da cobrança).

Uma chave é queimada por qualquer desfecho — inclusive erro. Depois que uma chamada registra um resultado (nota emitida ou um erro retentável), reenviar a mesma Idempotency-Key apenas devolve aquele mesmo desfecho — nunca emite de novo. Para retentar de verdade, mande a mesma raiz com um sufixo de retentativa: <chave>-r2, -r3, … A Nota Plena reconhece que é a mesma venda (mesma raiz) e a proteção de duplicidade garante que, se a tentativa anterior já tiver virado nota, você recebe a nota existente em vez de uma segunda. Veja Erros e retentativas.

Vale igual para a emissão de NFC-e (produtos, mod. 65): sem Idempotency-Key a resposta é 422. Uma repetição da requisição sem chave estável emitiria uma segunda nota para a mesma venda.

Bloqueio de cobrança (402)

Se a empresa estiver com cobrança em atraso, suspensa ou marcada como inativa pelo parceiro (partner_active_status=false no fluxo Surfbook), a emissão é negada com 402:

HTTP/1.1 402 Payment Required
{
  "detail": "Conta desativada pelo parceiro Surfbook — fale com o admin do Surfbook."
}

Outras mensagens possíveis no detail:

  • "Limite diário atingido enquanto a fatura está em atraso. Regularize em notaplena.com.br/billing."
  • "Conta suspensa por atraso de pagamento. Regularize em notaplena.com.br/billing."
  • "Conta suspensa. Regularize sua assinatura em notaplena.com.br/billing."

Chave de empresa: emitir e cancelar

Com a chave de empresa (plano Negócio) você emite e cancela NFS-e do seu próprio CNPJ. Por baixo é o mesmo motor da rota de parceiro: mesmo corpo, mesma resposta, mesma idempotência, mesmos erros estruturados, mesma cota e cobrança do plano, mesmo prazo de cancelamento e os mesmos webhooks. A diferença é que a chave já diz qual é a empresa — não há np_tenant_id para escolher.

Não existe sandbox para a chave de empresa. Sua empresa já está em produção: toda emissão bem-sucedida é uma nota fiscal real na prefeitura e conta na cota do plano. Teste o seu lado com valores reais de um serviço de verdade, ou cancele em seguida.

Emitir

POST/api/v1/integrations/tenant/nfse
curl -X POST https://api.notaplena.com.br/api/v1/integrations/tenant/nfse \
  -H "Authorization: Bearer np_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-7781" \
  -d '{
    "valor_servicos": "650.00",
    "discriminacao": "Consultoria — pacote 4 sessões",
    "codigo_servico": "10.05",
    "aliquota_iss": "0.02",
    "tomador_nome": "Ana Maria Pereira",
    "tomador_documento": "12345678901",
    "tomador_email": "ana@exemplo.com.br",
    "tomador_cep": "04023-062"
  }'

Resposta 200

{
  "emission_id": "9c1fa402-3c29-4bbb-9210-baf4cc2a4231",
  "status": "success",
  "numero_nfse": "1023",
  "codigo_verificacao": "NMTBGWKL",
  "numero_rps": "88",
  "serie_rps": "A",
  "valor_servicos": "650.00",
  "error_message": null,
  "error": null
}

Os campos aceitos e o significado de status são os da emissão de parceiro. O header Idempotency-Key é obrigatório: sem ele a resposta é 422 e nada é emitido. Reenviar a mesma Idempotency-Key devolve a mesma nota — nunca uma segunda. Se você mandar np_tenant_id, ele precisa ser o da sua empresa; qualquer outro responde 404.

Cancelar

POST/api/v1/integrations/tenant/nfse/{emission_id}/cancel
curl -X POST https://api.notaplena.com.br/api/v1/integrations/tenant/nfse/9c1fa402-3c29-4bbb-9210-baf4cc2a4231/cancel \
  -H "Authorization: Bearer np_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Serviço não prestado — pedido estornado",
    "confirm_outside_window": false
  }'

Resposta 200

{
  "success": true,
  "error_message": null,
  "requires_extra_confirmation": false,
  "days_since_emission": 0
}
  • reason é obrigatório (mínimo 3 caracteres) — vai para a prefeitura como motivo. Vazio responde 422.
  • Nota de um mês anterior, ainda dentro de 180 dias: a primeira chamada volta success: false com requires_extra_confirmation: true. Repita com confirm_outside_window: true para confirmar.
  • Nota com mais de 180 dias (6 meses): o cancelamento comum não é mais permitido. A resposta é success: false e o error_message explica o caminho — nota de substituição ou processo na prefeitura.
  • Falha de comunicação com a prefeitura: 502. Repita a chamada — ela confere o estado antes de pedir de novo.

Webhooks da chave de empresa

Mesmos eventos e mesma assinatura HMAC descritos em Webhooks, em rotas próprias da chave de empresa. Cada chave vê só as próprias assinaturas.

POST/api/v1/integrations/tenant/webhooks
GET/api/v1/integrations/tenant/webhooks
DELETE/api/v1/integrations/tenant/webhooks/{subscription_id}
curl -X POST https://api.notaplena.com.br/api/v1/integrations/tenant/webhooks \
  -H "Authorization: Bearer np_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://seu-erp.com.br/webhooks/nota-plena", "events": [] }'

Erros específicos da chave de empresa

  • 402 com error.code = "PLAN_NOT_ELIGIBLE" — a empresa não está mais no plano Negócio. A chave continua existindo (dá para ler e revogar), mas não emite nem cancela.
  • 402 com a mensagem de cobrança — cota ou assinatura (mesmas mensagens de Bloqueio de cobrança).
  • 404 — nota ou empresa que não é da sua chave.
  • 403 — chave de parceiro nestas rotas (parceiros usam /integrations/nfse).
  • 429 — limite de 120 requisições por minuto por chave.

Ler uma NFS-e

GET/api/v1/integrations/emissions/{emission_id}

Retorna os dados de uma NFS-e específica. emission_id é o UUID retornado por POST /nfse ou consultado no painel.

curl https://api.notaplena.com.br/api/v1/integrations/emissions/9c1fa402-... \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

Resposta 200

{
  "id": "9c1fa402-3c29-4bbb-9210-baf4cc2a4231",
  "created_at": "2026-04-12T15:42:00-03:00",
  "status": "success",
  "numero_nfse": "1023",
  "codigo_verificacao": "NMTBGWKL",
  "tomador_nome": "Ana Maria Pereira",
  "tomador_documento": "12345678901",
  "valor_servicos": "650.00",
  "discriminacao": "Consultoria — pacote 4 sessões",
  "pdf_url": "https://nfe.prefeitura.sp.gov.br/contribuinte/notaprint.aspx?nf=1023&verificacao=NMTBGWKL&ccm=02595273"
}

pdf_url aponta para o PDF oficial da prefeitura quando disponível (atualmente só São Paulo). Para outras cidades, o PDF pode ser obtido pela URL exibida no painel.

Sincronização de status do parceiro

Para parceiros que operam o ciclo de vida da empresa (ex.: Surfbook, que tem o relacionamento financeiro com o professor), a Nota Plena espelha o flag de atividade do parceiro no gate de emissão. Se você marcar a empresa como inativa, ela não consegue emitir — mesmo que a cobrança NP esteja em dia.

O partner_slug na URL é o slug lowercase do parceiro (ex.: surfbook). Slug que não bate com o dono da chave responde 404. O cnpj é só dígitos, 14 caracteres.

Empurrar status (parceiro → NP)

POST/api/v1/integrations/partners/{partner_slug}/tenants/{cnpj}/active-status

Chame a cada mudança de estado da empresa no seu sistema (assinatura ativada, cancelada, cartão negado, etc.). Idempotente — mandar duas vezes a mesma coisa só atualiza last_sync_at.

curl -X POST https://api.notaplena.com.br/api/v1/integrations/partners/surfbook/tenants/65851791000109/active-status \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "active": true,
    "reason": "Teacher reactivated their Surfbook account"
  }'

Resposta 200

{
  "active": true,
  "last_sync_at": "2026-05-10T22:14:33Z",
  "source": "push"
}

active (boolean, obrigatório) — true = empresa habilitada a emitir; false = emissão bloqueada com 402. reason (string, opcional, até ~200 chars) fica no log de auditoria.

Ler status (NP → parceiro)

GET/api/v1/integrations/partners/{partner_slug}/tenants/{cnpj}/active-status

Estado atual do flag para uma empresa.

curl https://api.notaplena.com.br/api/v1/integrations/partners/surfbook/tenants/65851791000109/active-status \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

Resposta 200

{
  "active": true,
  "last_sync_at": "2026-05-10T22:14:33Z",
  "source": "push"
}

source indica a origem do último update: "push" (parceiro chamou o POST acima), "pull" (cron diário NP buscou no sistema do parceiro como backup) ou "unknown" (nunca sincronizou).

Padrão de retry: em caso de 5xx ou timeout, faça backoff exponencial (1s, 5s, 30s, depois desista — o cron diário NP captura o que faltou). Não tente de novo em 4xx; são erros permanentes. Latência típica é ~200ms.

Webhooks

Webhooks são scopados por chave de parceiro (não por empresa) — uma única subscrição recebe eventos de todas as empresas que aquela chave opera. Cada subscrição traz um secret usado para assinar o payload (HMAC-SHA256), e o secret é exibido uma única vez no momento da criação.

Eventos disponíveis

  • nfse.emitted — NFS-e emitida com sucesso (também em sandbox, para você testar o receiver).
  • nfse.failed — emissão recusada pela prefeitura.
  • nfse.cancelled — NFS-e cancelada.
  • nfce.emitted / nfce.failed / nfce.cancelled — os mesmos três desfechos para a NFC-e (autorizada pela SEFAZ, rejeitada, cancelada).
  • cert.expiring_30d — certificado A1 cruzou pela primeira vez a barreira de 30 dias para expirar (uma só vez por certificado).

Garantias de entrega

  • Cada desfecho chega. Se o envio de um evento se perder entre o registro da nota e o agendamento do webhook (queda do servidor, erro de banco), uma varredura periódica percebe a falta e envia o evento — com o mesmo payload.
  • Uma vez por nota. O mesmo evento sobre a mesma nota (event + emission_id) é enfileirado no máximo uma vez por subscrição. Ainda assim, trate o receiver como idempotente por emission_id: uma entrega cuja confirmação HTTP se perdeu é reenviada.
  • "failed" só quando é certeza. Quando o desfecho é incerto — a requisição pode ter chegado à prefeitura ou à SEFAZ e a resposta se perdeu (NFC-e com error_code transport e retriable: false, ou duplicate_risk_unverifiable) — nenhum webhook é enviado, porque a nota pode existir. Se a consulta depois confirmar que ela foi autorizada, chega o *.emitted dela. Não reemita com outra chave nesse intervalo — a resposta HTTP da emissão já traz o error_code para você mostrar o estado.

Criar subscrição

POST/api/v1/integrations/webhooks
curl -X POST https://api.notaplena.com.br/api/v1/integrations/webhooks \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://meusistema.com.br/webhooks/notaplena",
    "events": ["nfse.emitted", "nfse.failed"]
  }'

events vazio ([]) = subscrever a todos. Evento desconhecido responde 422.

Resposta 201 — única vez que o secret é exibido

{
  "id": "5e1c-...-fb04",
  "url": "https://meusistema.com.br/webhooks/notaplena",
  "events": ["nfse.emitted", "nfse.failed"],
  "secret": "OK0FmQwL...64chars...",
  "last_success_at": null,
  "last_failure_at": null,
  "last_failure_reason": null,
  "revoked_at": null,
  "created_at": "2026-05-10T22:14:33Z"
}

Listar subscrições

GET/api/v1/integrations/webhooks
curl https://api.notaplena.com.br/api/v1/integrations/webhooks \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

O secret não é retornado em GETs — só na criação. Perdeu? Revogue e crie outra.

Revogar subscrição

DELETE/api/v1/integrations/webhooks/{subscription_id}
curl -X DELETE https://api.notaplena.com.br/api/v1/integrations/webhooks/5e1c-... \
  -H "Authorization: Bearer np_sk_demo_xxxxxxxx"

Catálogo público de planos

GET/api/v1/billing/catalog

Endpoint público (sem autenticação) com os preços atuais dos planos Nota Plena. Útil para mostrar preço em landing page ou em comparativo dentro do seu produto.

curl https://api.notaplena.com.br/api/v1/billing/catalog

Resposta 200

{
  "annual_enabled": false,
  "pix_enabled": false,
  "plans": [
    {
      "plan": "mei",
      "monthly_card_cents": 3900,
      "monthly_pix_cents": 3900,
      "annual_card_cents": null,
      "annual_pix_cents": null,
      "included_nfs": 20,
      "included_nfs_lifetime": null,
      "overage_card_cents": 100,
      "overage_pix_cents": 100
    },
    { "plan": "pro", ... },
    { "plan": "negocio", ... }
  ]
}
annual_enabled é false enquanto o ciclo anual está suspenso: annual_card_cents e annual_pix_cents voltam null e POST /billing/checkout com billing_cycle: "annual" responde 400. Trate os campos anuais como anuláveis.
pix_enabled é false enquanto pagamento por Pix não está disponível: POST /billing/checkout com payment_method: "pix" responde 501 e os campos *_pix_cents são apenas informativos (iguais aos de cartão). Não ofereça Pix na sua UI enquanto o campo for false.
Cobrança do cliente final: os endpoints GET /billing/me, POST /billing/checkout, GET /billing/portal e PATCH /billing/payment-method existem mas exigem login de usuário (não chave de parceiro). Eles são consumidos pela própria UX em notaplena.com.br/billing — parceiros normalmente não precisam tocar nesses endpoints.

Erros e retentativas

A regra de ouro: o status HTTP diz de quem é o problema. Nem toda falha é sua, e retentar da forma errada ou emite uma nota duplicada ou só repete o mesmo erro. Abaixo, cada desfecho e a ação certa.

Prefeitura offline — 503 PREFEITURA_UNAVAILABLE

A prefeitura não respondeu (timeout / servidor fora do ar). Não é erro do seu payload — o pedido está correto e é seguro retentar. Resposta:

HTTP/1.1 503 Service Unavailable
{
  "detail": {
    "message": "O servidor da prefeitura está offline. Tente novamente mais tarde com uma Idempotency-Key de RETENTATIVA — a mesma raiz com sufixo -r2, -r3 …",
    "error": { "code": "PREFEITURA_UNAVAILABLE", "retriable": true }
  }
}
Retente com a raiz + sufixo, não com a mesma chave. Este 503 já registrou a sua Idempotency-Key com o erro. Reenviar a mesma chave só devolve este mesmo erro (replay idempotente) — não reemite. Volte com a mesma raiz + -r2, -r3… e a proteção de duplicidade cuida do resto: se a tentativa anterior tiver virado nota, você recebe a nota existente. Este 503 não traz Retry-After — espere alguns segundos/minutos por conta própria antes de mandar o -rN.

Empresa pausada no município — 503 EMISSAO_PAUSADA_MUNICIPIO

Um disjuntor: a prefeitura recusou várias emissões seguidas desta empresa por um motivo que não está nesta nota (ex.: cadastro municipal), então pausamos automaticamente as emissões dessa empresa naquele município para não queimar RPS em silêncio. Resposta:

HTTP/1.1 503 Service Unavailable
Retry-After: 900
{
  "detail": {
    "message": "<motivo da prefeitura, verbatim>",
    "error": { "code": "EMISSAO_PAUSADA_MUNICIPIO", "retriable": true },
    "retry_after_seconds": 900
  }
}
Aqui a mesma chave funciona. Nenhuma nota, RPS ouIdempotency-Key foi consumida (o pedido nem chegou a ser persistido). Respeite o header Retry-After (segundos; também em detail.retry_after_seconds) e reenvie o mesmo payload / mesma chave. Qualquer nota que passe fecha o disjuntor antes do prazo. Não marque a venda como falha definitiva — o bloqueio se cura sozinho.

Duplicidade não verificável — 422 DUPLICATE_RISK_UNVERIFIABLE

Uma tentativa anterior desta mesma venda (mesma raiz de Idempotency-Key) pode ter chegado à prefeitura e não sabemos se virou nota. Bloqueamos para não emitir uma segunda. Não retente às cegas. Confira no portal da prefeitura ou em notaplena.com.br/problemas e, se de fato não existir nota, emita novamente.

Chave reusada para outra venda — 422 IDEMPOTENCY_KEY_REUSED

Você mandou uma Idempotency-Key que já pertence a outra nota, com payload diferente (outro tomador, valor, etc.). Nenhuma nota nova foi emitida. Use uma chave nova para esta venda; para retentar a venda anterior, use a mesma raiz com sufixo -rN.

Recusa da prefeitura — HTTP 200, status: "error"

O payload chegou e a prefeitura recusou por um problema de dados (CPF/CNPJ, código de serviço, alíquota, endereço…). Vem como 200 com status: "error" e o objeto error estruturado. Corrija o dado conforme error.hint e emita de novo com uma chave nova (ou -rN). Retentar sem corrigir só repete a recusa.

Validação — 422

Recusado antes de qualquer ida à prefeitura: campo obrigatório ausente, tipo errado, certificado ausente/expirado (NO_CERT / CERT_EXPIRED), inscrição municipal faltando (NO_INSCRICAO_MUNICIPAL), cidade não suportada (CITY_UNSUPPORTED) ou cod_tributacao_nacional obrigatório no Emissor Nacional (CTRIBNAC_MISSING), ou um cod_tributacao_nacional que não existe na tabela oficial (CTRIBNAC_INVALID). Corrija o payload — retentar igual não muda nada.

Cobrança — 402

Emissão bloqueada por cobrança em atraso, conta suspensa ou partner_active_status=false. Não é retentável até regularizar; mostre a mensagem de detail ao usuário.

Em processamento — status: "pending"

A emissão está em andamento e ainda não tem número (lote assíncrono; ou, na NFC-e, uma tentativa com a mesma Idempotency-Key ainda em andamento na SEFAZ — error_code: "in_flight"). Não reenvie. Aguarde o webhook (nfse.emitted/nfse.failed ou nfce.emitted/nfce.failed) — a própria tentativa em voo vai reportar o desfecho.

Resumo: retentar?

DesfechoRetentar?Como
503 PREFEITURA_UNAVAILABLESimMesma raiz + -rN (a mesma chave só repete o erro)
503 EMISSAO_PAUSADA_MUNICIPIOSim, após Retry-AfterMesma chave / mesmo payload
422 DUPLICATE_RISK_UNVERIFIABLESó após conferirVerifique no portal / /problemas; se não houver nota, emita
422 IDEMPOTENCY_KEY_REUSEDSim, com chave novaChave nova para a venda nova; -rN para a anterior
422 IDEMPOTENCY_KEY_REQUIREDSim, com chaveEnvie o header Idempotency-Key (estável por venda)
422 validação / cert / cidadeSó após corrigirAjuste o payload conforme hint
200 status: "error"Só após corrigir os dadosCorrija conforme error.hint, chave nova ou -rN
402 cobrançaNão até regularizarResolva a cobrança / status do parceiro
status: "pending"NãoAguarde o webhook — não reenvie
429 rate limitSimBackoff; respeite os 120 req/min por chave

Códigos de erro

StatusSignificadoO que fazer
400Request inválido (JSON malformado, header faltando)Confira o payload e os headers antes de tentar de novo
401Chave ausente, mal formada ou revogadaVerifique o header e regenere se necessário
402Emissão bloqueada por cobrança — conta em atraso, suspensa, ou partner_active_status=falseMostre a mensagem em detail ao usuário; oriente a regularizar em /billing
403Token de share não autoriza essa NF, ou chave de empresa em endpoint de parceiroConfira o tipo de chave; o JWT do share é amarrado a um único emission_id
404Recurso não existe — ou pertence a outro parceiro (acesso cruzado também é 404 para não vazar existência)Confirme o UUID/CNPJ e que a chave é dona do recurso
409Conflito de estado — CNPJ duplicado, ou activate-production sem cumprir os pré-requisitosLeia o detail — explica o que falta antes de tentar de novo
422Validação falhou — campo obrigatório, tipo errado, certificado expirado ou inválido, cidade não suportadaAjuste o payload conforme indicado em detail
429Rate limit excedido (120 req/min por chave de parceiro)Aguarde alguns segundos e tente novamente
503O nosso lado está indisponível para esta emissão — prefeitura offline (PREFEITURA_UNAVAILABLE) ou emissões automáticas pausadas para esta empresa naquele município (EMISSAO_PAUSADA_MUNICIPIO). O seu pedido está correto; detail.error.retriable é true.Não marque a venda como falha definitiva. Os dois casos retentam de formas diferentes — o pausado respeita o Retry-After e reusa a chave; o offline exige uma raiz -rN. Detalhes em Erros e retentativas
502Falha de transporte no cancelamento de NFC-e (error_code: "transport")Retry com backoff; a nota não foi cancelada
5xxErro no servidor NP ou falha temporária da prefeituraRetry com backoff exponencial (1s, 5s, 30s) — mas na emissão use uma raiz -rN, não a mesma Idempotency-Key (a chave já está registrada). Se persistir por mais de 5 min, escreva para suporte

Receita: integrar com Surfbook (ou similar)

  1. Ao onboardar um novo cliente no seu sistema, chame POST /integrations/tenants com o CNPJ dele. Guarde o id retornado — é o np_tenant_id que você vai usar para tudo daqui para a frente.
  2. Conduza o cliente para a página de onboarding da Nota Plena para subir o certificado A1, ou — se o seu fluxo já tem o .pfx em mãos — chame POST /tenants/{id}/certs.
  3. Faça uma emissão de teste com POST /integrations/nfse (em sandbox). Quando der success, chame activate-production para virar a empresa para produção.
  4. A cada cobrança paga / assinatura mudada no seu lado, dispare POST /integrations/nfse com um Idempotency-Key derivado do ID da cobrança (seguro para retries).
  5. A cada mudança de estado da empresa no seu sistema (cancelou, reativou, cartão negado), chame POST /integrations/partners/{slug}/tenants/{cnpj}/active-status.
  6. Para botões "Ver NF" no seu sistema, chame POST /share-link, abra a URL em nova aba. Faça cache por ~25 dias para evitar refazer em todo clique.

Dúvidas ou pedidos de integração? Escreva para atendimento@notaplena.com.br.

Com a sua autorização, usamos Meta Pixel e tag do Google para entender de onde vêm as visitas e se os nossos anúncios funcionam. Isso envia o seu endereço IP e a página que você está vendo para Meta Platforms, Inc. e Google LLC. Nada disso carrega antes de você decidir, e recusar não muda nada no site nem no uso da plataforma. Política de Privacidade.