# TOOB Notas API — referência canônica para IA e desenvolvedores Contrato: v0.3 · publicado em 2026-08-24. Esta é a fonte de verdade pública da API v1. Leia este arquivo antes de escrever uma integração. Se uma capacidade não estiver descrita aqui, não a suponha existente. Para diagnosticar uma cópia em cache ou confirmar a versão servida, rode: \`\`\`bash curl -I https://api.notas.toob.com.br/llms-full.txt \`\`\` O \`ETag\` e o \`Last-Modified\` devem mudar quando este contrato mudar. ## Objetivo e segurança A TOOB Notas recebe pedidos de emissão de NFS-e pela API. Em produção, o certificado A1 e a transmissão fiscal permanecem na infraestrutura fiscal da TOOB; a chave API identifica a organização. Nunca envie API keys ao navegador, não as coloque em repositórios, logs, prompts, tickets ou screenshots. \`\`\` Base URL: https://api.notas.toob.com.br/v1 Auth: Authorization: Bearer toob_live_... (produção) Authorization: Bearer toob_test_... (sandbox) JSON: Content-Type: application/json Dinheiro: inteiros em centavos (R$ 150,00 = 15000) \`\`\` Crie a chave em **Integrações** no painel. Guarde-a uma única vez em \`TOOB_API_KEY\`, uma variável de ambiente do servidor. Revogar uma chave no painel tem efeito imediato. ## Dois ambientes, um contrato Use primeiro uma chave \`toob_test_\`. No sandbox, nada vai para Prefeitura, nenhuma numeração fiscal é consumida, certificado não é aberto, nenhum e-mail é enviado e nenhum lançamento real é feito. As rotas, validações e respostas são as mesmas da produção. Ao concluir a integração, troque a chave por \`toob_live_\` **e prepare o catálogo live**: os IDs de clientes e serviços não atravessam ambientes. Toda nota devolve \`livemode\`: \`false\` para simulação e \`true\` para documento real. O sandbox pode receber \`sandbox_scenario\` no POST de NFS-e: | Valor | Resultado | | --- | --- | | \`authorized\` | fluxo autorizado simulado (padrão) | | \`rejected\` | rejeição simulada para testar tratamento | | \`slow\` | processamento mais lento para testar polling/timeout | \`sandbox_scenario\` é recusado em chave live. Dados de teste vivem separados e expiram; IDs \`cus_...\` e \`srv_...\` do sandbox não servem em produção. Você pode criar serviços de teste com \`POST /v1/services\` autenticado por uma chave \`toob_test_\`; eles ficam somente no catálogo de teste e expiram em 30 dias. Antes do live, crie ou selecione novamente o serviço fiscal confirmado no catálogo live. ## Modelo: catálogo primeiro Uma NFS-e sempre usa um **serviço cadastrado** por \`service_id\`. O serviço é o catálogo fiscal: descrição, município, códigos tributários, alíquota e valor padrão. A nota preserva um snapshot do serviço usado, portanto uma alteração posterior no catálogo só afeta emissões futuras. Existem duas formas corretas de obter o \`service_id\`: 1. **O painel é dono do catálogo.** Chame \`GET /v1/services\`, selecione o item já configurado no TOOB e persista o \`id\` retornado (\`srv_...\`) no seu sistema. 2. **A integração é dona do catálogo.** Liste antes de criar. Se não houver o item, chame \`POST /v1/services\` uma única vez, persista o \`id\` retornado e use-o nas emissões. A criação não recebe \`reference\` e não deduplica por conteúdo; integrações concorrentes devem proteger essa etapa com um lock ou transação própria. Alterações explícitas usam \`PATCH /v1/services/{id}\`. Não envie código fiscal, município ou um objeto \`service\` completo para inventar um serviço na emissão. O único override permitido é \`service.description\`, para detalhar uma competência/cobrança sem mudar o catálogo. Para o tomador, o caminho recomendado é enviá-lo diretamente na emissão. Não é necessário criar um \`customer_id\` antes: - envie \`customer.document\`, \`customer.name\` e o endereço já validado pela sua aplicação: \`zip_code\`, \`street\`, \`number\` e \`district\`; - a TOOB usa o CEP apenas para resolver internamente o código IBGE do município. Ela não sobrescreve nome, logradouro ou bairro enviados pela integração; - \`customer.municipality_code\` é um campo avançado e opcional. Se informado, ele precisa corresponder ao CEP; em divergência, a API devolve HTTP 422 \`municipality_code_conflict\`; - \`customer_id\` é o caminho avançado para quem quer administrar o próprio catálogo e reutilizar um \`cus_...\` já completo. A emissão cria ou reutiliza o cadastro fiscal pelo documento, mas não altera um cadastro existente. Use \`PATCH /v1/customers/{id}\` para mudar dados de propósito. Sem \`customer_id\` ou os dados inline completos, a emissão é inválida. Não envie uma NFS-e incompleta: registre a pendência no seu sistema, complete os dados fiscais do tomador e só então chame a API. ## Idempotência por reference Toda emissão exige \`reference\`: o identificador estável da fatura, pedido ou cobrança no sistema integrador. Ela é a idempotência pública — não use \`Idempotency-Key\`. - mesma chave API + mesma \`reference\` + mesmo payload: devolve a mesma nota, sem segunda emissão; - mesma \`reference\` com payload diferente: HTTP 409 \`idempotency_conflict\`; - nova cobrança: nova \`reference\`. Guarde a referência antes de chamar a API e a reutilize após timeout/retry. ### Exemplo: cobrança recorrente com Stripe No webhook \`invoice.paid\`, use a invoice — e não a entrega do webhook — como identidade da cobrança. Exemplo de referência: \`stripe_invoice_\`. O \`event.id\` identifica apenas uma tentativa de entrega do Stripe e não deve ser a idempotência fiscal. Depois do HTTP 202, grave a NFS-e local como \`queued\`; a emissão fiscal continua de forma assíncrona. \`\`\`json { "reference": "stripe_invoice_in_123", "customer": { "document": "39053344705", "name": "Cliente Exemplo", "address": { "zip_code": "01310100", "street": "Avenida Paulista", "number": "100", "district": "Bela Vista" } }, "service_id": "srv_...", "amount_cents": 150000 } \`\`\` \`amount_cents\` é o valor da prestação que será documentado em centavos. Em uma integração Stripe, só use \`invoice.amount_paid\` se ele for esse valor fiscal da prestação; descontos, retenções e composição tributária exigem a regra fiscal confirmada da empresa, não uma inferência automática da API. ## Fluxo de implementação 1. Crie \`TOOB_API_KEY\` de teste no painel. 2. Liste ou crie o serviço e persista \`service_id\`. 3. Para o happy path, envie os dados completos do tomador na emissão. Só use \`customer_id\` se seu sistema for dono do catálogo de clientes. 4. Faça \`POST /v1/nfse\`. 5. Grave \`id\`, \`reference\` e \`livemode\`; consulte o status ou use webhooks. 6. Só após a aceitação no sandbox, crie uma chave live e faça uma validação controlada em produção. ## Quickstart \`\`\`bash # Serviço: liste primeiro. Só execute este POST uma vez quando o item não existir. curl https://api.notas.toob.com.br/v1/services \\ -H "Authorization: Bearer $TOOB_API_KEY" \\ -H "Content-Type: application/json" \\ -d '{ "name": "Consultoria mensal", "description": "Consultoria de software", "municipality_code": "3550308", "municipal_tax_code": "02804", "municipal_taxation": "T", "default_amount_cents": 150000 }' # Emissão: use o srv_... retornado ou obtido com GET /services. curl https://api.notas.toob.com.br/v1/nfse \\ -H "Authorization: Bearer $TOOB_API_KEY" \\ -H "Content-Type: application/json" \\ -d '{ "reference": "cobranca_2026_08_00042", "customer": { "document": "39053344705", "name": "Cliente de Teste", "address": { "zip_code": "01310100", "street": "Avenida Paulista", "number": "100", "district": "Bela Vista" } }, "service_id": "srv_SUBSTITUA_PELO_ID", "amount_cents": 150000, "sandbox_scenario": "authorized" }' \`\`\` Resposta de criação: HTTP 202. \`\`\`json { "id": "nfse_...", "status": "queued", "livemode": false, "idempotent": false, "reference": "cobranca_2026_08_00042", "customer_id": "cus_..." } \`\`\` A emissão é assíncrona. Consulte: \`\`\`bash curl https://api.notas.toob.com.br/v1/nfse/nfse_... \\ -H "Authorization: Bearer $TOOB_API_KEY" \`\`\` Estados possíveis: \`queued\`, \`processing\`, \`issued\`, \`rejected\`, \`cancelled\`. Trate \`issued\`, \`rejected\` e \`cancelled\` como terminais. ## Teste de integração sem Stripe Você não precisa cobrar nem configurar um listener Stripe para confirmar a conexão com a TOOB. Com \`TOOB_API_KEY=toob_test_...\`: 1. chame \`GET /v1/services\` para validar a chave e o ambiente; 2. se não houver um \`srv_...\` de teste, crie-o com \`POST /v1/services\` e guarde o id retornado; 3. envie uma NFS-e sandbox com \`sandbox_scenario: "authorized"\`; 4. depois do HTTP 202, consulte a nota até \`issued\` ou receba o webhook de sandbox, se você tiver configurado um endpoint. Esse caminho exercita autenticação, catálogo, validação de payload, fila e emissão assíncrona sem pagamento. Testar o recebimento do webhook do Stripe é um passo separado, feito com o modo de teste/CLI do Stripe ou um evento de teste no ambiente de staging. ## Endpoints ### Health \`GET /v1/health\` não exige autenticação e retorna \`{ "status": "ok" }\`. ### Serviços \`GET /v1/services?limit=&starting_after=\` Lista serviços. \`limit\` é de 1 a 100. Resposta: \`\`\`json { "data": [{ "id": "srv_...", "name": "Consultoria mensal", "description": "Consultoria de software", "municipality_code": "3550308", "municipal_tax_code": "02804", "municipal_taxation": "T", "default_amount_cents": 150000, "active": true, "created_at": "2026-08-23T12:00:00.000Z" }], "next_cursor": null } \`\`\` Passe o \`next_cursor\` como \`starting_after\` para a próxima página. \`POST /v1/services\` cria um novo serviço e devolve HTTP 201. Com chave \`toob_test_\`, ele cria um serviço de sandbox, válido só nesse ambiente. Ele não possui chave de deduplicação: não trate POST repetido como reaproveitamento. Liste e persista o \`srv_...\` no seu sistema; se a sua integração recebe eventos concorrentes, proteja a primeira criação com lock ou transação local. Campos: | Campo | Obrigatório | Regra | | --- | --- | --- | | \`name\` | sim | 2–80 caracteres | | \`description\` | sim | 3–2000 caracteres | | \`municipality_code\` | sim | código IBGE com 7 dígitos | | \`default_amount_cents\` | sim | inteiro positivo | | \`national_tax_code\` | não | código nacional de serviço | | \`municipal_tax_code\` | não | 4 ou 5 dígitos | | \`municipal_taxation\` | não | \`T\` ou \`F\` | | \`iss_rate\` | não | > 0 e até 5 | | \`active\` | não | padrão true | \`GET /v1/services/{id}\` lê um serviço. \`PATCH /v1/services/{id}\` atualiza parcialmente os campos acima. Serviço inativo não pode emitir: 409 \`service_inactive\`. ### Clientes \`GET /v1/customers?limit=&starting_after=\` lista clientes com a mesma paginação. \`GET /v1/customers/{id}\` consulta um cliente. \`POST /v1/customers\` cria/sincroniza por documento. \`document\` e \`name\` são obrigatórios. Use esta rota somente quando a integração quiser administrar um catálogo de clientes e guardar \`cus_...\`; para uma emissão direta, envie o tomador inline em \`POST /v1/nfse\`. Campos opcionais: \`nickname\`, \`email\`, \`municipality_code\` e \`address\`: \`\`\`json { "document": "39053344705", "name": "Cliente Exemplo", "email": "financeiro@cliente.com", "municipality_code": "3550308", "address": { "street_type": "Rua", "street": "Exemplo", "number": "100", "district": "Centro", "zip_code": "01001000" } } \`\`\` \`PATCH /v1/customers/{id}\` atualiza parcialmente um cadastro. Use esta rota para alterar um cliente existente, não uma emissão. ### Endereço e município do tomador na emissão No happy path de \`POST /v1/nfse\`, envie como verdade o endereço que sua aplicação já possui e validou: \`customer.address.zip_code\`, \`customer.address.street\`, \`customer.address.number\` e \`customer.address.district\` são obrigatórios. \`complement\` e \`street_type\` são opcionais. A TOOB resolve o código IBGE a partir do CEP antes de emitir. Você não precisa pedir esse código ao seu usuário. Se a integração já tiver o IBGE, pode enviar \`customer.municipality_code\` (sete dígitos) como opção avançada. Quando CEP e código forem informados, eles devem representar o mesmo município; a API não escolhe um deles silenciosamente. Em divergência, ela responde HTTP 422 com \`code: "municipality_code_conflict"\` e \`field: "customer.municipality_code"\`. Se a consulta de CEP estiver temporariamente indisponível, um \`municipality_code\` explícito permite continuar a emissão; quando a consulta responder, a consistência entre os dois campos continua sendo verificada. Isso não é uma consulta de CNPJ/CPF: a TOOB não substitui os dados pessoais ou o endereço que a integração enviou por uma fonte externa. Em especial, informe o nome para CPF e CNPJ. Se o seu sistema usar \`customer_id\`, mantenha o cadastro fiscal daquele cliente completo antes de emitir. ### Emissão \`POST /v1/nfse\` exige: | Campo | Obrigatório | Observação | | --- | --- | --- | | \`reference\` | sim | idempotência por cobrança, até 200 caracteres | | \`service_id\` | sim | \`srv_...\` ativo e pertencente à organização | | \`amount_cents\` | sim | inteiro positivo em centavos | | \`customer_id\` | condicional | caminho avançado: \`cus_...\` com cadastro fiscal completo | | \`customer.document\` + \`customer.name\` | condicional | happy path; alternativa a \`customer_id\`; cria/reutiliza cadastro pelo documento | | \`customer.address.zip_code\`, \`street\`, \`number\`, \`district\` | sim com \`customer\` inline | endereço validado pela integração; CEP resolve o IBGE internamente | | \`customer.municipality_code\` | não | IBGE avançado; se enviado, precisa coincidir com o CEP | | \`customer.address.complement\`, \`street_type\` | não | complemento e tipo de logradouro | | \`service.description\` | não | detalhe pontual da nota, não muda o serviço | | \`iss_withheld\` | não | ISS retido | | \`sandbox_scenario\` | só teste | \`authorized\`, \`rejected\` ou \`slow\` | Campos desconhecidos são rejeitados. O serviço precisa pertencer à chave e estar ativo; cliente e serviço de ambientes diferentes não podem ser usados. \`GET /v1/nfse?limit=&starting_after=\` lista notas. \`GET /v1/nfse/{id}\` consulta uma nota. \`GET /v1/nfse/{id}/xml\` baixa seu XML quando disponível. \`POST /v1/nfse/{id}/cancel\` recebe: \`\`\`json { "reason_code": "1", "reason_text": "Descrição clara do erro que exige cancelamento." } \`\`\` \`reason_code\` aceita \`1\` (erro na emissão), \`2\` (serviço não prestado) ou \`9\` (outros); \`reason_text\` tem de 15 a 255 caracteres. ## Defaults da chave e webhooks No painel de cada chave, a organização define se a TOOB envia e-mail ao tomador após a nota estar autorizada, registra lançamento no fechamento e confirma a operação municipal padrão quando aplicável. Essas escolhas não pertencem ao corpo de uma emissão. A emissão não confirma entrega de e-mail: acompanhe a NFS-e até \`issued\` antes de considerar o documento disponível. Configure webhooks no painel para receber \`nfse.issued\`, \`nfse.rejected\` e \`nfse.cancelled\`. O destino precisa ser público e HTTPS. Verifique o header \`toob-signature\` antes de processar; responda 2xx rapidamente e deduplique entregas pelo id do evento. ## Erros, retries e limites Erro padrão: \`\`\`json { "error": { "code": "invalid_request", "message": "service_id é obrigatório.", "field": "service_id", "retryable": false, "request_id": "req_..." } } \`\`\` - Se \`retryable\` for \`false\`, corrija antes de repetir. - Se for \`true\`, respeite \`Retry-After\` em HTTP 429 e mantenha a mesma \`reference\` para um retry de emissão. - Para suporte, informe \`request_id\`; nunca informe a chave. ## Não disponível - SDK oficial publicado em npm; - DANFSE/PDF pela API; - quaisquer endpoints, campos ou automatismos que não apareçam neste arquivo.