← Voltar ao blog

Inscrição Estadual SP: algoritmo passo a passo e gerador em TypeScript

·12 min de leitura

A Inscrição Estadual de São Paulo tem dois dígitos verificadores com pesos assimétricos e um prefixo textual que não entra nos cálculos. Implementações incorretas costumam tratar o prefixo "P" como dígito ou copiar o algoritmo de outro estado. Este artigo documenta o formato oficial da SEFAZ-SP, implementa gerador e validador em TypeScript sem dependências externas, e mostra como integrar IEs válidas em pipelines de teste fiscal.

O que é a Inscrição Estadual e por que SP tem seu próprio algoritmo

A Inscrição Estadual (IE) identifica o contribuinte no cadastro estadual de ICMS. Cada Unidade Federativa define seus próprios pesos e regras de verificação. A SEFAZ-SP usa dois dígitos verificadores com pesos diferentes entre si, o que distingue o algoritmo paulista dos estados que usam dígito único ou pesos simétricos.

Diferença entre IE, CNPJ e CNAE

CNPJ identifica a pessoa jurídica perante a Receita Federal. IE identifica o estabelecimento no cadastro estadual de ICMS. CNAE classifica a atividade econômica. São documentos complementares: uma NF-e de saída exige tanto o CNPJ do emitente quanto a IE do estabelecimento. Para montar um emissor completo com campos correlacionados, use o gerador de empresa. Para gerar o CNPJ isolado, acesse o /gerador-cnpj.

Quem é obrigado a ter IE-SP

Contribuintes de ICMS em São Paulo são obrigados a se inscrever no Cadastro de Contribuintes (Decreto-Lei Estadual 45.490/2000, art. 19). Isso inclui industriais, comerciantes, transportadores intermunicipais e produtores rurais. Prestadores de serviços tributados por ISS em geral estão dispensados.

Por que dados de IE válidos são necessários em testes fiscais

Softwares de emissão de NF-e validam o dígito verificador da IE antes de transmitir o XML à SEFAZ. Se o número falha na verificação local, o documento é rejeitado antes de chegar ao servidor de autorização. Ambientes de homologação aceitam IEs sinteticamente válidas, o que torna geradores como este necessários para testes de ponta a ponta.

Anatomia do número: estrutura dos 12 dígitos

Formato oficial da SEFAZ-SP: P.XXX.XXX.XXX.XXX

A SEFAZ-SP formata a IE como P.XXX.XXX.XXX.XXX. O número bruto, sem o prefixo e sem pontos, tem exatamente 12 dígitos numéricos distribuídos em quatro grupos de três.

O prefixo "P" e o que ele indica

O "P" é um prefixo literal que identifica o tipo de inscrição padrão (pessoa jurídica comercial ou industrial). Ele não é um dígito e não entra em nenhum cálculo de verificação. Aparece na representação textual do Sintegra e nos campos de IE da NF-e.

Posição dos dois dígitos verificadores (D10 e D12)

Nos 12 dígitos numéricos, a distribuição é:

PosiçãoConteúdo
1 a 8Dígitos base, gerados aleatoriamente
9Dígito fixo (0 nesta implementação)
10Primeiro verificador (D10)
11Dígito fixo (0 nesta implementação)
12Segundo verificador (D12)

Casos especiais: substitutos tributários e produtores rurais

Substitutos tributários de outros estados inscritos em SP e produtores rurais usam prefixos ou formatos diferentes do "P" comercial. O algoritmo descrito aqui cobre o caso mais comum em testes de NF-e: contribuintes comerciais e industriais com inscrição padrão.

Algoritmo do primeiro dígito verificador (D10)

Pesos aplicados às posições 1 a 8: [1, 3, 4, 5, 6, 7, 8, 10]

Cada um dos oito dígitos base recebe um peso fixo na mesma ordem: a posição 1 recebe peso 1, a posição 2 recebe peso 3, e assim até a posição 8, que recebe peso 10. O somatório dos produtos é dividido por 11.

function calcD10(digits: number[]): number {
  const weights = [1, 3, 4, 5, 6, 7, 8, 10];
  const sum = weights.reduce((acc, w, i) => acc + w * digits[i], 0);
  const r = sum % 11;
  return r < 2 ? 0 : r;
}

Cálculo do somatório e regra do mod 11

O resultado de sum % 11 é o dígito verificador, com uma exceção descrita a seguir.

Regra de exceção: resto 0 e resto 1 resultam em dígito 0

Tanto o resto 0 quanto o resto 1 produzem D10 = 0. Uma implementação que só trate r === 0 gera dígitos incorretos quando o somatório for congruente a 1 (mod 11).

Exemplo manual com valores reais passo a passo

Base: [1, 2, 3, 4, 5, 6, 7, 8]

1×1 + 2×3 + 3×4 + 4×5 + 5×6 + 6×7 + 7×8 + 8×10
= 1 + 6 + 12 + 20 + 30 + 42 + 56 + 80
= 247
247 % 11 = 5  →  D10 = 5

Algoritmo do segundo dígito verificador (D12)

Pesos aplicados às posições 1, 3, 4, 5, 6, 7, 8, 10 (subconjunto diferente)

D12 não usa todas as posições 1 a 8. Usa as posições 1, 3, 4, 5, 6, 7, 8 mais a posição 10 (o D10 já calculado). As posições 2 e 9 são ignoradas. Os pesos correspondentes são [3, 2, 10, 9, 8, 7, 6, 5].

function calcD12(full: number[]): number {
  const positions = [0, 2, 3, 4, 5, 6, 7, 9]; // 0-indexed (1-indexed: 1,3,4,5,6,7,8,10)
  const weights   = [3, 2, 10, 9, 8, 7, 6, 5];
  const sum = weights.reduce((acc, w, i) => acc + w * full[positions[i]], 0);
  const r = sum % 11;
  return r < 2 ? 0 : r;
}

Regra do mod 11 para D12 e sua independência de D10

D12 usa o valor de D10 como um dos termos do somatório, mas os dois cálculos são independentes: não há recursão e não existe risco de circularidade. A regra de exceção é idêntica: resto menor que 2 produz dígito 0.

Exemplo manual complementando o caso anterior

Com base [1, 2, 3, 4, 5, 6, 7, 8] e D10 = 5, o array completo antes de D12 é:

full = [1, 2, 3, 4, 5, 6, 7, 8, 0, 5, 0, ?]
posições usadas (0-idx): 0→1, 2→3, 3→4, 4→5, 5→6, 6→7, 7→8, 9→5
pesos:                       3    2   10    9    8    7    6    5
produtos:           3 + 6 + 40 + 45 + 48 + 49 + 48 + 25 = 264
264 % 11 = 0  →  D12 = 0

IE completa: P.123.456.780.500

Implementação do gerador em TypeScript

Função generateIESP(): geração dos 8 dígitos base aleatórios

function generateIESP(): string {
  const base = Array.from({ length: 8 }, () => Math.floor(Math.random() * 10));

  const full = [...base, 0, calcD10(base), 0, 0];
  full[11] = calcD12(full);

  const n = full.join('');
  return `P.${n.slice(0, 3)}.${n.slice(3, 6)}.${n.slice(6, 9)}.${n.slice(9, 12)}`;
}

Funções calcD10() e calcD12(): isolamento dos cálculos

As duas funções são puras e sem efeitos colaterais. Isolá-las do gerador permite reutilizá-las no validador sem duplicação de lógica.

Montagem final com formatação P.XXX.XXX.XXX.XXX

O join('') transforma o array de 12 dígitos em string contígua. Os quatro slice() extraem cada grupo de três para compor o formato P.d1d2d3.d4d5d6.d7d8d9.d10d11d12.

Exportar como módulo reutilizável (sem dependências externas)

// ie-sp.ts
export { calcD10, calcD12, generateIESP, validateIESP };

Importável em Node.js, Deno ou Bun sem nenhum pacote adicional.

Implementação do validador em TypeScript

Função validateIESP(ie: string): boolean

function validateIESP(ie: string): boolean {
  const clean = ie.replace(/^P\.?/i, '').replace(/[\s.]/g, '');

  if (clean.length !== 12 || !/^\d{12}$/.test(clean)) return false;

  const d = clean.split('').map(Number);
  return d[9] === calcD10(d.slice(0, 8)) && d[11] === calcD12(d);
}

Sanitização da entrada: remover P, pontos, espaços

A regex ^P\.? remove o prefixo "P" com ou sem ponto. O segundo replace remove espaços e pontos restantes. Após a sanitização, a string deve ter exatamente 12 caracteres numéricos.

Verificação de comprimento e caracteres numéricos

/^\d{12}$/.test(clean) garante que não há letras ou caracteres especiais após a sanitização. Entradas como "P.111.111.111.111" passam na sanitização mas falham na verificação do dígito.

Reaproveitamento de calcD10() e calcD12() no validador

O validador chama as mesmas funções do gerador. Se os cálculos estiverem corretos no gerador, o validador não pode divergir.

DICA: Em formulários de NF-e, aplique o validador no evento onBlur, não no onChange. Validação a cada tecla em campos de 12 dígitos gera experiência ruim sem ganho funcional.

Casos de borda que quebram implementações ingênuas

IE de produtor rural começa com dígito diferente de P

Produtores rurais em SP usam uma faixa de inscrição com prefixo numérico diferente do "P" comercial. Uma regex ^P rejeita essas IEs válidas. Separe os dois casos de uso antes de implementar validação de prefixo.

Zeros à esquerda no campo base precisam ser preservados

IEs podem ter zeros nos primeiros dígitos base, como P.001.234.567.890. Converter a string para Number antes de processar apaga esses zeros. Sempre use arrays de dígitos ou strings com padding explícito.

Strings como "111.111.111.111" passam na validação de formato mas falham no dígito

111.111.111.111 tem 12 dígitos e formato sintático válido. Para base [1,1,1,1,1,1,1,1] com pesos [1,3,4,5,6,7,8,10]: 1+3+4+5+6+7+8+10 = 44, 44 % 11 = 0 → D10 = 0, não 1. O validador rejeita corretamente.

Diferença entre IE inválida e IE inexistente no Sintegra

Validação local confirma que o número é matematicamente possível. Não confirma que o contribuinte existe e está ativo. Para consulta de situação cadastral real, use a API do Sintegra ou a consulta pública da SEFAZ-SP.

Testes unitários para o gerador e validador

Suite com casos conhecidos da SEFAZ-SP (valores de referência públicos)

import { describe, it, expect } from 'vitest';
import { generateIESP, validateIESP } from './ie-sp';

describe('IE-SP', () => {
  it('aceita IE do exemplo da documentação', () => {
    expect(validateIESP('P.123.456.780.500')).toBe(true);
  });

  it('rejeita IE com comprimento incorreto', () => {
    expect(validateIESP('P.123.456.780.50')).toBe(false);
  });

  it('rejeita IE com dígito verificador errado', () => {
    expect(validateIESP('P.111.111.111.111')).toBe(false);
  });
});

Property-based testing: todo número gerado deve passar no validador

it('propriedade: todo generateIESP() retorna IE válida', () => {
  for (let i = 0; i < 1000; i++) {
    expect(validateIESP(generateIESP())).toBe(true);
  }
});

Teste de roundtrip: gerar → formatar → validar → true

O loop de 1.000 iterações cobre o roundtrip mínimo. Para testes baseados em propriedades com fast-check:

import fc from 'fast-check';

it('fast-check roundtrip', () => {
  fc.assert(fc.property(fc.constant(null), () => validateIESP(generateIESP())));
});

Uso em pipelines de teste fiscal e NF-e

Seed de banco com IEs válidas para testes de emissão de NF-e

-- Generated by FakeForge BR - free Brazilian test data
-- https://fakeforge.com.br
CREATE TABLE IF NOT EXISTS empresas_seed (
  id           SERIAL PRIMARY KEY,
  razao_social VARCHAR(100),
  cnpj         CHAR(14),
  ie_sp        VARCHAR(20)
);

INSERT INTO empresas_seed (razao_social, cnpj, ie_sp) VALUES
  ('Distribuidora Teste Ltda', '12345678000195', 'P.123.456.780.500'),
  ('Comercio Exemplo SA',      '98765432000101', 'P.234.567.891.602');

Uso com gerador de CNPJ para montar emissor completo

Para compor emissor completo (CNPJ + IE + endereço correlacionado), use o gerador de empresa. Para validar o CNPJ antes de associar a IE, use o /validar-cnpj. A partir de 01/07/2026, o formato muda para CNPJ alfanumérico (IN RFB 2.229/2024).

Exportar via API REST com format=sql para scripts de seed

curl "https://fakeforge.com.br/api/generate?type=ie-sp&quantity=10&format=sql"

O endpoint retorna um bloco CREATE TABLE seguido dos INSERT com os registros. Consulte a documentação da API para parâmetros de autenticação e rate limits.

LGPD Art. 7º, IX — base legal para uso de dados sintéticos em homologação

Dados gerados sinteticamente sem correspondência a pessoas reais não configuram dado pessoal pela LGPD Art. 5º, I. O uso em homologação pode ser fundamentado em LGPD Art. 7º, IX (legítimo interesse), mas a ausência de dado pessoal torna desnecessária qualquer base legal para o tratamento.

Comparação com outros estados: quando o algoritmo muda

RJ, MG e RS têm pesos diferentes — não reutilize a função SP

O Rio de Janeiro usa 8 dígitos base com pesos [2, 7, 6, 5, 4, 3, 2, 1] e um único dígito verificador. Minas Gerais tem 13 dígitos com cálculo em duas passagens. O Rio Grande do Sul usa mod 11 com pesos distintos. Reutilizar calcD10() ou calcD12() nesses estados produz validações incorretas de forma silenciosa.

Tabela rápida: estados com dígito único vs. dois dígitos verificadores

UFDígitos totaisVerificadoresObservação
SP122 (D10, D12)Prefixo P na formatação
RJ81Mod 11, pesos distintos
MG132Duas passagens de cálculo
RS101Mod 11 padrão
PR102Verificadores ao final
SC91Mod 11 simples
AVISO: Esta tabela é uma referência rápida. Confirme o formato exato no manual de integração da SEFAZ de cada estado antes de usar em produção.

Por que vale encapsular por estado (ieValidatoruf)

const ieValidator: Record<string, (ie: string) => boolean> = {
  SP: validateIESP,
  // RJ: validateIERJ,
  // MG: validateIEMG,
};

function validateIE(uf: string, ie: string): boolean {
  return ieValidator[uf]?.(ie) ?? false;
}

Encapsular por UF elimina if/else espalhados no código de emissão de NF-e e torna a adição de novos estados uma questão de inserir uma entrada no objeto.

Resumo

  • Gerador: gera 8 dígitos aleatórios, aplica calcD10() com pesos [1,3,4,5,6,7,8,10] e calcD12() com subconjunto de posições [1,3,4,5,6,7,8,10] e pesos [3,2,10,9,8,7,6,5], formata como P.XXX.XXX.XXX.XXX.
  • Validador: sanitiza prefixo P e pontos, verifica comprimento 12, reaplica as mesmas funções de cálculo e compara os resultados com as posições 10 e 12 do número.
  • Armadilhas: preservar zeros à esquerda, tratar restos 0 e 1 como dígito 0, não confundir validação matemática com consulta de situação cadastral no Sintegra.
  • Testes: rodar 1.000 iterações de roundtrip generateIESP() seguido de validateIESP() como smoke test mínimo antes de integrar em qualquer pipeline.
  • Integração fiscal: compor CNPJ via /gerador-cnpj com IE e endereço via /gerador-empresa; exportar seed com API REST e format=sql.
  • Multi-UF: encapsular validadores por estado em Record<string, validator> para escalar sem ramificação condicional no código de emissão de NF-e.

Perguntas frequentes

Como integrar validação IE-SP em um form React sem bloquear a experiência?+

Validação matemática é síncrona e <1ms. Use onBlur, não onChange para evitar validação a cada tecla. Para consulta real ao Sintegra, faça async via API Route em background com debounce; isso não bloqueia o form. Padrão: validação local imediata + fetch ao Sintegra em paralelo com retry exponencial e timeout de 5s.

Vale validar IE-SP no cliente ou é melhor fazer apenas na API?+

Validação matemática é O(1) em ambos (>10k/ms). Trade-off é segurança: cliente é rápido mas bypassável; API é seguro. Best practice: validação local no cliente para UX, re-validação na API antes de INSERT obrigatoriamente. Função é pura e <100 bytes minificado, cabe em ambos sem penalidade.

Como lidar com IEs de produtores rurais que começam com número?+

Produtores rurais SP usam prefixo numérico em vez de 'P'. O algoritmo D10/D12 é idêntico, mas o sanitizador e validação de prefixo mudam. Crie `validateIESPProdutor()` separada ou parameterize com `{ type: 'produtor' }`. Confirme prefixos numéricos válidos na documentação SEFAZ-SP antes de deployar.

Devo validar IE-SP antes de enviar a NF-e para a SEFAZ?+

Sim, é exatamente o caso de uso. No middleware de validação de NF-e XML, aplique `validateIESP()` antes de enviar à SEFAZ. Pega erros de digitação localmente, economiza requisição de autorização e reduz rejeições de protocolo. Combine com `validateCNPJ()` do emissor para garantir coerência.

Como testar se um IE-SP gerado existe de verdade no Sintegra?+

Números gerados têm check digits corretos mas não existem no Sintegra. Para testes end-to-end, use API pública do Sintegra ou consulta de situação cadastral com IEs conhecidas em ambiente de teste da SEFAZ-SP. Alternativamente, mock a resposta e teste integração com Vitest + nock.