Documentação

Emita NFS-e pelo seu sistema

Uma chamada HTTP por nota. O WAY NF monta a DPS com o perfil da empresa, assina com o certificado A1 guardado, envia à Sefin Nacional e avisa o seu sistema quando a nota sai.

Como funciona

O seu sistema continua dono do financeiro (cobranças, pagamentos, alunos, clientes). O WAY NF só transforma "esta cobrança foi paga" em NFS-e na Sefin Nacional.

1. Conta no WAY NF
A empresa cria a conta no painel (login com e-mail). É por ela que você vê todas as notas, inclusive as emitidas pela API, baixa XML e cancela.
2. Configurar emissão (uma vez)
No painel: dados do prestador, código do serviço, tributação e certificado A1. O sistema de vocês nunca precisa saber disso.
3. Criar a chave
Em Integrações (API): uma chave para o seu sistema e, se quiser, o endereço do webhook.
4. Seu financeiro chama a API
Quando uma cobrança é paga (ou quando o usuário clica em "Emitir nota"), o seu sistema manda tomador, valor, competência e descrição.
5. O WAY NF emite e avisa
Em segundos a nota sai na Sefin Nacional; o webhook avisa o seu sistema com a chave de acesso, ou com o motivo da recusa.
6. Seu sistema mostra o resultado
Grave a chave de acesso junto da cobrança e mostre "Nota emitida" ao usuário. Quem preferir pode acompanhar tudo só pelo painel do WAY NF.

Conectar o financeiro do seu sistema

Onde cada coisa do seu financeiro entra na API. Exemplo real: o Way School (gestão escolar) emite a nota da mensalidade quando a secretaria clica em "Emitir" numa parcela paga.

Cobrança/parcela paga
POST /v1/requests com Idempotency-Key = id da cobrança
Responsável financeiro (nome, CPF/CNPJ)
payload.taker.name e payload.taker.tax_id
Valor pago
service_amount ("150.00")
Mês de referência / vencimento
competence (AAAA-MM-DD)
Texto da nota (ex.: mensalidade de setembro, aluno X)
payload.service.description
Situação da nota na tela do seu sistema
webhook (request.issued / rejected / cancelled) ou GET /v1/requests/{id}
Número/chave da nota guardado na cobrança
access_key do webhook
Botão "Cancelar nota"
POST /v1/requests/{id}/cancel

O que o seu sistema precisa ter: um lugar para guardar a chave (variável de ambiente no servidor), uma coluna para o id/chave da nota em cada cobrança e uma rota que recebe o webhook e confere a assinatura. Nada de certificado, XML ou regra fiscal.

Dúvidas

Quem usa a API também entra no painel?
Sim. Toda empresa tem conta no painel: é lá que configura a emissão, cria as chaves e vê todas as notas (as da API aparecem junto, marcadas como API). A API é só o jeito do seu sistema pedir notas sem ninguém digitar.
Meu sistema precisa guardar certificado ou senha?
Não. O A1 fica cifrado no WAY NF. O seu sistema guarda só a chave wnf_… (como segredo, no servidor, nunca no navegador).
E se a chamada der timeout?
Repita com o mesmo Idempotency-Key. O WAY NF devolve o mesmo pedido e nunca emite duas notas para a mesma chave.
E se o webhook falhar?
O WAY NF tenta de novo até 8 vezes. Como rede de segurança, consulte GET /v1/requests?external_id=… das notas que ainda estão em andamento.
A nota foi recusada. E agora?
O motivo vem em message (ex.: CPF inválido). Corrija o dado no seu sistema e envie um pedido novo com outro Idempotency-Key.
Como testo sem emitir nota de verdade?
Use a chave wnf_test_: emite em homologação da Sefin, sem valor fiscal. Quando estiver tudo certo, a Way libera a produção e você cria a chave wnf_live_.
Posso emitir para várias empresas (CNPJs)?
Cada empresa tem a sua chave. Um sistema com várias escolas/filiais guarda uma chave por empresa.

Começar em 3 passos

  1. No painel da empresa, em Configurar emissão, salve o perfil de emissão e instale o certificado A1.
  2. Em Integrações (API), crie uma chave. wnf_test_… emite em homologação (sem valor fiscal); wnf_live_… emite de verdade e só existe depois que a Way libera a produção da empresa.
  3. Envie as notas com Authorization: Bearer <chave>. A chave já identifica a empresa e o ambiente.

Emitir — POST /v1/requests

Você manda só o que muda em cada nota: tomador, valor, competência e descrição. Prestador, códigos de serviço e tributação vêm do perfil da empresa.

curl -X POST https://way-nf.vercel.app/v1/requests \
  -H "Authorization: Bearer $WAYNF_CHAVE" \
  -H "Idempotency-Key: cobranca-2026-09-123" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "cobranca-2026-09-123",
    "competence": "2026-09-25",
    "service_amount": "150.00",
    "payload": {
      "taker": { "name": "Maria da Silva", "tax_id": "12345678909", "email": "maria@exemplo.com" },
      "service": { "description": "Mensalidade de setembro" }
    }
  }'

Resposta 202. Com a fila ativa a nota sai em segundos: acompanhe por consulta ou webhook.

{
  "id": "cmuh71ozv0001l7044lsugi9f",
  "external_id": "cobranca-2026-09-123",
  "status": "queued",
  "dps_number": "20003",
  "status_url": "/v1/requests/cmuh71ozv0001l7044lsugi9f",
  "correlation_id": "2a02a63a-0e08-4fd5-8d39-d27ba5b9a4d3"
}
Node.js
const resp = await fetch("https://way-nf.vercel.app/v1/requests", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WAYNF_CHAVE}`,
    "Idempotency-Key": cobranca.id,           // repetir não duplica a nota
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    external_id: cobranca.id,
    competence: "2026-09-25",
    service_amount: cobranca.valor.toFixed(2),  // "150.00"
    payload: { taker: { name: aluno.responsavel, tax_id: aluno.cpf }, service: { description: "Mensalidade de setembro" } },
  }),
});
const pedido = await resp.json(); // pedido.status: "queued" | "issued" | ...
Python
import os, requests

resp = requests.post(
    "https://way-nf.vercel.app/v1/requests",
    headers={"Authorization": f"Bearer {os.environ['WAYNF_CHAVE']}", "Idempotency-Key": cobranca_id},
    json={
        "external_id": cobranca_id,
        "competence": "2026-09-25",
        "service_amount": "150.00",
        "payload": {"taker": {"name": "Maria da Silva", "tax_id": "12345678909"},
                    "service": {"description": "Mensalidade de setembro"}},
    },
    timeout=30,
)
pedido = resp.json()

Consultar e baixar XML

curl https://way-nf.vercel.app/v1/requests/{id} -H "Authorization: Bearer $WAYNF_CHAVE"
curl "https://way-nf.vercel.app/v1/requests?external_id=cobranca-2026-09-123" -H "Authorization: Bearer $WAYNF_CHAVE"
curl "https://way-nf.vercel.app/v1/requests/{id}/xml?type=nfse" -H "Authorization: Bearer $WAYNF_CHAVE" -o nota.xml

type do XML: nfse (nota autorizada), dps (declaração enviada) ou cancellation (evento de cancelamento). O cabeçalho X-Content-SHA256 traz o hash para conferência.

Cancelar — POST /v1/requests/{id}/cancel

curl -X POST https://way-nf.vercel.app/v1/requests/{id}/cancel \
  -H "Authorization: Bearer $WAYNF_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{ "reason_code": 1, "reason": "Valor da mensalidade cobrado errado" }'

reason_code: 1 erro na emissão, 2 serviço não prestado, 9 outros. reason: 15 a 255 caracteres. A resposta traz cancellation: "requested"; a situação vira cancelled quando a Sefin registra. Recusa (ex.: prazo vencido) mantém issued com message.

Webhooks

Cadastre um endereço https:// na integração. O WAY NF chama esse endereço quando a nota é emitida, recusada, fica incerta, é cancelada ou tem o cancelamento recusado. Responda com qualquer 2xx; sem isso, tentamos de novo com espera crescente (até 8 vezes). O mesmo aviso pode chegar mais de uma vez: use X-Way-NF-Event-ID para ignorar repetidos.

POST https://seu-sistema.com/webhooks/way-nf
X-Way-NF-Event-ID: request.issued:cmuh71ozv0001l7044lsugi9f
X-Way-NF-Timestamp: 1790354843000
X-Way-NF-Signature: 5f1c…(hex)

{
  "id": "request.issued:cmuh71ozv0001l7044lsugi9f",
  "schema_version": "1",
  "request_id": "cmuh71ozv0001l7044lsugi9f",
  "data": {
    "event": "request.issued",
    "external_id": "cobranca-2026-09-123",
    "status": "issued",
    "environment": "test",
    "access_key": "33045572234483523000158000000000000926097606683075",
    "dps_number": "20003",
    "issued_at": "2026-09-25T16:47:23.000Z"
  }
}

Confira a assinatura com o segredo mostrado ao ativar o webhook:

import { createHmac, timingSafeEqual } from "node:crypto";

// corpo = texto bruto recebido (antes de JSON.parse)
function avisoValido(corpo, headers, segredo) {
  const ts = headers["x-way-nf-timestamp"];
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return false; // evita reenvio antigo
  const esperado = createHmac("sha256", segredo).update(`${ts}.${corpo}`).digest("hex");
  const recebido = headers["x-way-nf-signature"] ?? "";
  return recebido.length === esperado.length && timingSafeEqual(Buffer.from(recebido), Buffer.from(esperado));
}

Situações da nota

accepted
Recebido, mas a empresa ainda não configurou a emissão (message explica o que falta).
queued
Na fila; sai em segundos.
processing
Sendo enviado à Sefin.
issued
Nota autorizada: access_key, dps_number e issued_at preenchidos.
rejected
Recusada pela Sefin; message traz o código e o motivo. Corrija e envie um pedido novo.
uncertain
A Sefin não respondeu com clareza. O WAY NF consulta sozinho e resolve; não reenvie.
cancelled
Cancelada na Sefin.

Erros

Corpo: { "code", "message", "correlation_id" }. Mande o correlation_id ao suporte.

400
JSON ou parâmetros inválidos; chave cobre mais de uma empresa sem X-Company-Id.
401
Chave ausente, errada ou revogada.
403
A chave não tem essa permissão (emitir ou consultar) ou o ambiente não bate.
404
Pedido não existe para esta chave.
409
A mesma Idempotency-Key foi usada com outros dados.
422
Dados recusados (valor, data, CPF/CNPJ, motivo de cancelamento…).
429
Mais de 60 chamadas por minuto; espere o Retry-After.

Regras importantes

  • Idempotência: use um Idempotency-Key estável por cobrança (ex.: o id dela). Repetir a chamada por timeout nunca gera nota duplicada.
  • external_id é único por chave: serve para achar a nota depois (GET /v1/requests?external_id=…).
  • Valores em reais como texto com ponto e duas casas ("150.00"); CPF/CNPJ só com dígitos.
  • Limite de 60 chamadas por minuto por chave.
  • Opcionais: X-Company-Id e X-Fiscal-Environment, só se uma chave antiga cobrir mais de uma empresa ou ambiente.