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_xxxxxxxxxxxxxxxxxxxxxxxx404 — 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.brTodos 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.
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
/api/v1/integrations/tenantsCria 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
/api/v1/integrations/tenantsRetorna 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
/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
/api/v1/integrations/tenants/{tenant_id}/configAtualiza 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
/api/v1/integrations/tenants/{tenant_id}/activate-productionVira a empresa de sandbox para production. Para ser aprovada, a empresa precisa ter:
- certificado A1 válido e não expirado;
inscricao_municipalpreenchida;- regime fiscal consistente (se
is_mei=true, entãosimples_ativoeanexoem "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".
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
/api/v1/integrations/tenants/{tenant_id}/certsSobe 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
}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)
/api/v1/integrations/nfseEmite 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).
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.
Emitir
/api/v1/integrations/tenant/nfsecurl -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
/api/v1/integrations/tenant/nfse/{emission_id}/cancelcurl -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 responde422.- Nota de um mês anterior, ainda dentro de 180 dias: a primeira chamada volta
success: falsecomrequires_extra_confirmation: true. Repita comconfirm_outside_window: truepara confirmar. - Nota com mais de 180 dias (6 meses): o cancelamento comum não é mais permitido. A resposta é
success: falsee oerror_messageexplica 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.
/api/v1/integrations/tenant/webhooks/api/v1/integrations/tenant/webhooks/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
402comerror.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.402com 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
/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)
/api/v1/integrations/partners/{partner_slug}/tenants/{cnpj}/active-statusChame 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)
/api/v1/integrations/partners/{partner_slug}/tenants/{cnpj}/active-statusEstado 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).
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.Magic-link SSO (parceiro → NP logado)
Quando seu usuário clica em "emitir nota fiscal" no seu sistema, ele deve cair logado no NP sem digitar senha. Você gera um link de uso único, prazo de 5 minutos, e redireciona o browser para a URL retornada.
Você já autenticou o usuário do seu lado — confiamos na sua chave de parceiro para afirmar "esse e-mail é desse usuário". A Nota Plena acha ou cria o registro do User no NP, conecta ele à empresa via TenantUser (papel OWNER se ainda não existir), assina um JWT curto e persiste a linha de uso único.
Endpoint
/api/v1/integrations/partners/{partner_slug}/login-linkcurl -X POST https://api.notaplena.com.br/api/v1/integrations/partners/surfbook/login-link \
-H "Authorization: Bearer np_sk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"cnpj": "65851791000109",
"user_email": "professor@example.com",
"redirect_to": "/emit"
}'Resposta 201
{
"url": "https://notaplena.com.br/auth/magic#token=...&redirect_to=/emit",
"expires_at": "2026-05-11T14:25:33Z"
}#, não de ?. Isso é de propósito: o navegador não envia o fragmento a servidor nenhum, então o token não aparece em log de acesso de proxy, CDN ou monitoramento no caminho. Consequência prática: repasse a URL inteira, sem reconstruir. Se o seu código tentar ler o token como parâmetro de query, ou remontar a URL campo a campo, o fragmento se perde e o usuário cai numa tela sem token.cnpj (obrigatório, 14 dígitos) — empresa do seu tenant. user_email (obrigatório) — o e-mail do usuário. redirect_to (opcional, default /dashboard) — caminho same-origin para onde o usuário deve cair depois de entrar; precisa começar com / e não pode conter ://.
Como usar
Mande o usuário para a url imediatamente. O token vale por 5 minutos e exatamente um uso:
// Backend handler do seu botão "emitir nota fiscal":
const r = await fetch('https://api.notaplena.com.br/api/v1/integrations/partners/surfbook/login-link', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NP_PARTNER_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ cnpj, user_email, redirect_to: '/emit' }),
});
const { url } = await r.json();
res.redirect(302, url); // ou window.open(url, '_blank') no front400 already_used_or_unknown.Erros
404— sem empresa sua com esse CNPJ.400—redirect_tonão é caminho same-origin.422—user_emailausente ou corpo inválido.401— chave de parceiro inválida ou ausente.
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 poremission_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_codetransporteretriable: false, ouduplicate_risk_unverifiable) — nenhum webhook é enviado, porque a nota pode existir. Se a consulta depois confirmar que ela foi autorizada, chega o*.emitteddela. Não reemita com outra chave nesse intervalo — a resposta HTTP da emissão já traz oerror_codepara você mostrar o estado.
Criar subscrição
/api/v1/integrations/webhookscurl -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
/api/v1/integrations/webhookscurl 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
/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
/api/v1/billing/catalogEndpoint 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/catalogResposta 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.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 }
}
}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
}
}Idempotency-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?
| Desfecho | Retentar? | Como |
|---|---|---|
503 PREFEITURA_UNAVAILABLE | Sim | Mesma raiz + -rN (a mesma chave só repete o erro) |
503 EMISSAO_PAUSADA_MUNICIPIO | Sim, após Retry-After | Mesma chave / mesmo payload |
422 DUPLICATE_RISK_UNVERIFIABLE | Só após conferir | Verifique no portal / /problemas; se não houver nota, emita |
422 IDEMPOTENCY_KEY_REUSED | Sim, com chave nova | Chave nova para a venda nova; -rN para a anterior |
422 IDEMPOTENCY_KEY_REQUIRED | Sim, com chave | Envie o header Idempotency-Key (estável por venda) |
| 422 validação / cert / cidade | Só após corrigir | Ajuste o payload conforme hint |
200 status: "error" | Só após corrigir os dados | Corrija conforme error.hint, chave nova ou -rN |
| 402 cobrança | Não até regularizar | Resolva a cobrança / status do parceiro |
status: "pending" | Não | Aguarde o webhook — não reenvie |
| 429 rate limit | Sim | Backoff; respeite os 120 req/min por chave |
Códigos de erro
| Status | Significado | O que fazer |
|---|---|---|
400 | Request inválido (JSON malformado, header faltando) | Confira o payload e os headers antes de tentar de novo |
401 | Chave ausente, mal formada ou revogada | Verifique o header e regenere se necessário |
402 | Emissão bloqueada por cobrança — conta em atraso, suspensa, ou partner_active_status=false | Mostre a mensagem em detail ao usuário; oriente a regularizar em /billing |
403 | Token de share não autoriza essa NF, ou chave de empresa em endpoint de parceiro | Confira o tipo de chave; o JWT do share é amarrado a um único emission_id |
404 | Recurso 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 |
409 | Conflito de estado — CNPJ duplicado, ou activate-production sem cumprir os pré-requisitos | Leia o detail — explica o que falta antes de tentar de novo |
422 | Validação falhou — campo obrigatório, tipo errado, certificado expirado ou inválido, cidade não suportada | Ajuste o payload conforme indicado em detail |
429 | Rate limit excedido (120 req/min por chave de parceiro) | Aguarde alguns segundos e tente novamente |
503 | O 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 |
502 | Falha de transporte no cancelamento de NFC-e (error_code: "transport") | Retry com backoff; a nota não foi cancelada |
5xx | Erro no servidor NP ou falha temporária da prefeitura | Retry 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)
- Ao onboardar um novo cliente no seu sistema, chame
POST /integrations/tenantscom o CNPJ dele. Guarde oidretornado — é onp_tenant_idque você vai usar para tudo daqui para a frente. - 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. - Faça uma emissão de teste com
POST /integrations/nfse(em sandbox). Quando dersuccess, chameactivate-productionpara virar a empresa para produção. - A cada cobrança paga / assinatura mudada no seu lado, dispare
POST /integrations/nfsecom umIdempotency-Keyderivado do ID da cobrança (seguro para retries). - 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. - 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.