O emissaoja emite Nota Fiscal de Serviço eletrônica (NFS-e) no Emissor Nacional com o CNPJ e a senha do portal, sem certificado digital, para MEI e empresas do Simples Nacional. Esta API expõe o mesmo motor que o app usa: cadastre a empresa uma vez, crie tomadores e serviços, emita e cancele notas, baixe PDF e XML.
Tudo em JSON, em português, em snake_case. A referência completa, com todos os campos e códigos, está em /v1/openapi.json (OpenAPI 3.1). Esta página também existe em texto puro: /docs.md e /llms.txt, para agentes de IA.
Em 5 minutos
- Entre no app com a sua conta Google e aceite os termos: emissaoja.com.br/app.
- Em Conta → API, crie uma chave. Ela aparece uma vez: guarde como senha.
- Conecte a empresa com o CNPJ e a senha do Emissor Nacional (passo abaixo). O emissaoja confere a senha no portal, lê o cadastro fiscal e traz as notas dos últimos 90 dias.
- Crie um tomador pelo CNPJ e emita a primeira nota.
# 1. Conectar a empresa (responde na hora; a verificação segue em segundo plano)
curl -X PUT https://emissaoja.com.br/v1/empresa/credencial \
-H "Authorization: Bearer ej_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{"cnpj":"11222333000181","senha":"senha-do-emissor-nacional"}'
# 2. Acompanhar até proximo_passo sair de "aguardar" (a cada 2 s, depois 5 s)
curl https://emissaoja.com.br/v1/empresa/credencial \
-H "Authorization: Bearer ej_live_SUA_CHAVE"
# 3. Tomador pelo CNPJ (nome e endereço vêm da Receita)
curl -X POST https://emissaoja.com.br/v1/tomadores \
-H "Authorization: Bearer ej_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{"documento":"12345678000195"}'
# 4. Emitir (prévia + emissão num passo). Idempotency-Key evita nota em dobro.
curl -X POST https://emissaoja.com.br/v1/notas \
-H "Authorization: Bearer ej_live_SUA_CHAVE" \
-H "Idempotency-Key: pedido-2026-10-0001" \
-H "Content-Type: application/json" \
-d '{"tomador_tipo":"pj","tomador_id":"<id do tomador>","valor":1500,"descricao":"Consultoria de outubro"}'
# 5. Acompanhar o pedido até autorizada | recusada | incerta
curl https://emissaoja.com.br/v1/emissoes/<id do pedido> \
-H "Authorization: Bearer ej_live_SUA_CHAVE"
A resposta do passo 4 é 202 com o pedido; a do passo 5 traz estado, numero, chave_acesso e nota: { origem, id }, o par que baixa o PDF em GET /v1/notas/{origem}/{id}/pdf.
Autenticação
Toda chamada leva a chave no cabeçalho:
Authorization: Bearer ej_live_…
- A chave vale para a empresa e o plano da conta que a criou. Não há escopos: quem tem a chave emite em nome da empresa. Guarde-a no servidor, nunca no navegador nem em app de celular.
- Até 5 chaves ativas por conta. Revogar uma chave (no app, Conta → API) para na hora.
- Sem chave, chave inválida ou revogada:
401comcode: "chave_invalida". - As rotas
GET /v1/statuseGET /v1/openapi.jsonnão exigem chave.
Respostas e erros
Sucesso é 2xx com JSON. Erro é sempre este envelope, e o que importa é o code, nunca o texto:
{ "status": "ERROR", "code": "nota_bloqueada", "message": "Antes de emitir, resolva:", "bloqueios": [ { "code": "tomador_incompleto", "mensagem": "…", "acao": "completar_tomador" } ] }
| HTTP | Quando | Códigos comuns |
|---|---|---|
| 400 | Campo inválido | validation_error, valor_invalido, tomador_obrigatorio, cnpj_invalido, chave_idempotencia_invalida |
| 401 | Chave | chave_invalida |
| 402 | Limite do plano no mês | limite_notas |
| 404 | Não existe | tomador_nao_encontrado, servico_nao_encontrado, nota_nao_encontrada, pedido_nao_encontrado |
| 409 | Estado atual não permite | sem_empresa, aceite_pendente, emissao_repetida_recente, possivel_duplicada, incerta_pendente, ultimo_servico_ativo |
| 422 | A nota não pode sair assim | nota_bloqueada (com bloqueios[]) |
| 429 | Limite por minuto | rate_limited (cabeçalho Retry-After) |
| 503 | Portal ou motor fora | motor_indisponivel, fila_indisponivel (repita com a mesma chave), receita_indisponivel |
A lista completa, com mensagens e ações, está no OpenAPI.
Limites
- 60 chamadas por minuto por chave; emissões 10 por minuto; buscas e catálogo 120.
- Verificação de senha: 3 falhas por CNPJ em 24 h (regra do portal, para não bloquear a conta da empresa), 5 por conta, 10 por IP.
- Nota conta no plano quando o Emissor Nacional aceita o pedido; recusada não conta. O plano Grátis tem 1 nota por mês. Veja os planos em emissaoja.com.br/#precos.
Empresa
Uma conta tem uma empresa. A API configura e acompanha o vínculo:
| Rota | O que faz |
|---|---|
GET /v1/empresa | Empresa, estado da credencial (ausente, verificando, ativa, congelada), estado do onboarding, importação e avisos |
PUT /v1/empresa/credencial | { cnpj, senha } → 202 com a tentativa. Dispara a verificação no portal e o retro-seed (cadastro fiscal + notas dos últimos 90 dias). A senha nunca é guardada nem devolvida |
GET /v1/empresa/credencial | A tentativa em andamento (ou a última): estado e proximo_passo (aguardar, configurando, pronto, corrigir_senha, tentar_mais_tarde, redefinir_no_portal, suporte) |
GET /v1/empresa/onboarding | Estágios, perguntas pendentes e bloqueio |
PUT /v1/empresa/fiscal | { aliquota_simples?, reg_ap_trib_sn? }: as perguntas que o cadastro fiscal não responde sozinho |
POST /v1/empresa/reler-fisco | Lê o cadastro fiscal de novo no portal |
onboarding.estado segue configurando → perguntas → pronto. Com perguntas, leia GET /v1/empresa/onboarding e responda em PUT /v1/empresa/fiscal. Emitir antes de pronto devolve 422 nota_bloqueada com o que falta.
Tomadores
| Rota | O que faz |
|---|---|
GET /v1/tomadores?busca=&pagina=&limite=&ordem= | Lista paginada (nome ou recentes) |
GET /v1/tomadores/busca?q= | Busca rápida por nome ou documento |
POST /v1/tomadores | { documento } para CNPJ (nome e endereço vêm da Receita) ou { documento, nome } para CPF. Já existia → 200 com ja_existia: true |
GET /v1/tomadores/{tipo}/{id} | Ficha (tipo = pj ou pf) |
PUT /v1/tomadores/{tipo}/{id} | Edição parcial de endereço e contato. O documento é imutável |
Tomador não se apaga: notas emitidas apontam para ele.
Serviços
| Rota | O que faz | ||
|---|---|---|---|
| `GET /v1/servicos?ativos=1 | 0 | todos` | Lista |
POST /v1/servicos | { descricao, c_trib_nac, trib_issqn, retem_iss, nbs?, c_trib_mun?, tp_oper?, aliquota_iss? } | ||
PUT /v1/servicos/{id} | Edição parcial | ||
DELETE /v1/servicos/{id} | Desativa (o último ativo não pode: ultimo_servico_ativo) | ||
GET /v1/catalogo/servicos?q= | Catálogo nacional de códigos de serviço, por texto |
trib_issqn aceita tributavel e nao_incidencia. Isenção, imunidade e exportação ficam fora do emissaoja.
Notas
| Rota | O que faz | ||
|---|---|---|---|
POST /v1/notas | Emitir: prévia + emissão. Cabeçalho Idempotency-Key obrigatório. 202 { pedido, previa } | ||
GET /v1/emissoes/{id} | Acompanhar o pedido: `na_fila → emitindo → confirmando → autorizada | recusada | incerta` |
GET /v1/notas?periodo=&situacao=&busca=&cursor= | Lista (locais e importadas), paginada por cursor | ||
GET /v1/notas/{origem}/{id}/pdf | DANFSe em PDF | ||
GET /v1/notas/{origem}/{id}/xml | XML da nota | ||
POST /v1/notas/{origem}/{id}/cancelar | `{ motivo: "1" | "2", justificativa? } → 202 com a operacao; acompanhe em GET /v1/operacoes/{id}` |
Corpo de POST /v1/notas:
| Campo | Obrigatório | Observação |
|---|---|---|
tomador_tipo, tomador_id | sim | pj ou pf + id do tomador |
valor | sim | Reais, até 2 casas |
servico_id | não | Omitido: o serviço principal |
descricao | não | Até 2000 caracteres; omitida: a descrição do serviço |
competencia | não | AAAA-MM-DD; omitida: hoje |
retem_iss, aliquota_iss, municipio_prestacao | não | Só quando um bloqueio pedir |
Nota não se altera: para corrigir, cancele e emita outra. Nota autorizada e depois cancelada conta no plano; recusada não.
Idempotência. Use uma Idempotency-Key por nota que você quer emitir (um id do seu pedido serve). Repetir a mesma chave devolve 200 com o mesmo pedido e repetido: true; nunca emite duas vezes. Se receber 503 fila_indisponivel, repita com a mesma chave.
Nota em dobro. Nota igual (mesmo tomador, valor e competência) no mesmo dia volta 409 possivel_duplicada com nota_igual_recente; se for mesmo outra, repita com permitir_repetida_de = o pedido_id informado. Mesma assinatura em menos de 2 minutos volta 409 emissao_repetida_recente; repita com confirmo_repeticao: true.
Ambiente de testes
https://sandbox.emissaoja.com.br é o mesmo emissaoja com um Emissor Nacional simulado: nada ali existe no fisco e nada é cobrado. Crie a conta com o Google, conecte uma das empresas de exemplo e use a API, o MCP e o app como em produção. As chaves do sandbox começam com ej_test_ e só valem lá; as de produção, ej_live_, só valem em produção. GET /v1/status diz "ambiente": "sandbox".
| CNPJ de exemplo | Senha | O que acontece |
|---|---|---|
11111111000191 | qualquer | MEI pronto para emitir, com notas e clientes dos últimos 90 dias |
22222222000191 | qualquer | ME/EPP que pede a alíquota do Simples antes de emitir |
33333333000191 | qualquer | MEI sem notas anteriores |
44444444000191 | qualquer | Empresa bloqueada (não optante do Simples) |
| qualquer um | errada | Senha recusada pelo portal |
Valores com significado ao emitir: 13,13 nota incerta (confira com POST /v1/emissoes/{id}/conferir → autorizada), 77,77 incerta que não saiu, 99,99 recusada pelo fisco. Tomadores de exemplo: CNPJ 12345678000195, CPF 52998224725. O sandbox é reiniciado a cada publicação do emissaoja: a empresa continua conectada, mas as notas emitidas antes somem.
Consulta de CNPJ
GET /v1/cnpj/{cnpj} devolve os dados cadastrais da Receita Federal (razão social, situação, CNAEs, endereço, Simples, MEI, sócios com CPF mascarado), pela base do apipj. Não cria tomador; serve para validar antes de cadastrar. 30 consultas por minuto por chave.
MCP: emitir pelo seu assistente
O emissaoja também é um servidor MCP (Model Context Protocol) em https://emissaoja.com.br/mcp: Claude Code, Cursor, VS Code e outros clientes que mandam um cabeçalho fixo conectam com a mesma chave de API. As ferramentas são as rotas desta API: ver a empresa, conectar a credencial, listar e criar tomadores, listar serviços, emitir, acompanhar, listar e cancelar notas, consultar CNPJ. Emitir e cancelar têm efeito fiscal real: o assistente confirma com você antes.
claude mcp add --transport http emissaoja https://emissaoja.com.br/mcp \
--header "Authorization: Bearer ej_live_SUA_CHAVE"
Cursor e VS Code (mcp.json):
{ "mcpServers": { "emissaoja": { "url": "https://emissaoja.com.br/mcp", "headers": { "Authorization": "Bearer ej_live_SUA_CHAVE" } } } }
No claude.ai (Configurações → Conectores → Adicionar conector personalizado) e no ChatGPT (modo desenvolvedor), informe só a URL https://emissaoja.com.br/mcp: o assistente pede para conectar, você entra com o Google e autoriza. A conexão vale 60 dias sem uso e pode ser desfeita em Conta → API.
Exemplos
JavaScript (Node 18+ ou qualquer runtime com fetch):
const BASE = "https://emissaoja.com.br";
const cabecalhos = { Authorization: `Bearer ${process.env.EMISSAOJA_KEY}`, "Content-Type": "application/json" };
async function emitir({ tomadorTipo, tomadorId, valor, descricao, chave }) {
const r = await fetch(`${BASE}/v1/notas`, {
method: "POST",
headers: { ...cabecalhos, "Idempotency-Key": chave },
body: JSON.stringify({ tomador_tipo: tomadorTipo, tomador_id: tomadorId, valor, descricao }),
});
const corpo = await r.json();
if (!r.ok) throw new Error(`${corpo.code}: ${corpo.message}`);
return corpo.pedido;
}
async function aguardar(pedidoId) {
for (;;) {
const r = await fetch(`${BASE}/v1/emissoes/${pedidoId}`, { headers: cabecalhos });
const { pedido } = await r.json();
if (["autorizada", "recusada", "incerta"].includes(pedido.estado)) return pedido;
await new Promise((ok) => setTimeout(ok, pedido.proxima_consulta_ms ?? 3000));
}
}
Python (requests):
import os, time, requests
BASE = "https://emissaoja.com.br"
H = {"Authorization": f"Bearer {os.environ['EMISSAOJA_KEY']}"}
def emitir(tomador_tipo, tomador_id, valor, descricao, chave):
r = requests.post(f"{BASE}/v1/notas", headers={**H, "Idempotency-Key": chave},
json={"tomador_tipo": tomador_tipo, "tomador_id": tomador_id, "valor": valor, "descricao": descricao}, timeout=30)
corpo = r.json()
if not r.ok:
raise RuntimeError(f"{corpo['code']}: {corpo['message']}")
return corpo["pedido"]
def aguardar(pedido_id):
while True:
pedido = requests.get(f"{BASE}/v1/emissoes/{pedido_id}", headers=H, timeout=30).json()["pedido"]
if pedido["estado"] in ("autorizada", "recusada", "incerta"):
return pedido
time.sleep((pedido.get("proxima_consulta_ms") or 3000) / 1000)
Perguntas
Precisa de certificado digital? Não. O emissaoja entra no Emissor Nacional com o CNPJ e a senha do próprio portal.
A senha fica guardada? Durante a verificação ela fica só na memória do servidor; depois, cifrada no motor fiscal, para emitir quando você pedir. Ela nunca aparece em resposta nem em log. Encerrar a conta apaga a senha.
Tem ambiente de testes? Sim: https://sandbox.emissaoja.com.br, com o motor simulado (veja a seção Ambiente de testes).
E webhooks? Ainda não; acompanhe o pedido em GET /v1/emissoes/{id} usando proxima_consulta_ms. Webhooks assinados estão no plano.
Posso ter várias empresas numa conta? Não. Uma conta Google tem uma empresa. Para várias empresas, uma conta e uma chave por empresa.
Dúvidas: suporte@emissaoja.com.br. O emissaoja é um serviço privado e não tem vínculo com a Receita Federal nem com o Emissor Nacional.