Documentação para desenvolvedores

API & MCP de orçamentos solares

Gere orçamentos de energia solar com a sua formação de preço — no seu site, no seu CRM ou no seu agente de IA. Uma chamada devolve potência, valor, payback e economia; opcionalmente grava a proposta e entrega um PDF e uma página interativa prontos. Sem SDK obrigatório: é HTTP + JSON.

Início rápido

Base URL: https://app.ionluz.com.br/api/v1. Autentique com a sua chave e faça uma cotação de simulação (não grava nada):

curl -X POST https://app.ionluz.com.br/api/v1/quote \
  -H "Authorization: Bearer ilz_sua_chave" \
  -H "Content-Type: application/json" \
  -d '{ "consumoKwhMes": 650, "uf": "SP", "fases": "bi", "preview": true }'
Dica: comece sempre com preview: true. Você calcula à vontade (calculadoras, testes, agentes de IA) sem criar leads. Quando quiser registrar a proposta e obter o PDF, use preview: false (o padrão da API é gravar).

Autenticação

Toda requisição usa a sua chave de API no cabeçalho Authorization, no formato Bearer. A chave começa com ilz_ e é gerada (e revogada) na página API & Integração do seu painel.

Authorization: Bearer ilz_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • A chave aparece uma única vez na criação — guarde em local seguro (variável de ambiente, cofre de segredos).
  • Guardamos apenas o hash (SHA-256): não conseguimos recuperar a chave; se perder, gere outra.
  • Envie a chave somente pelo header Authorization. Nunca em query string (?key=), que vaza em logs e histórico.
  • Cada integrador tem a sua chave, ligada à sua conta, aos seus produtos, kits e à sua formação de preço.

Limites & headers

As respostas trazem cabeçalhos de rastreio e de limite de uso. Os X-RateLimit-* aparecem nas respostas autenticadas (do limite por chave) — o 401 chave-invalida sai sem eles.

HeaderSignificado
X-Request-IdId único da requisição (também vem no corpo como requestId). Cite-o ao pedir suporte.
X-Api-VersionVersão da API (atualmente 1).
X-RateLimit-LimitMáximo de requisições na janela.
X-RateLimit-RemainingQuantas ainda restam na janela atual.
X-RateLimit-ResetSegundos até a janela reiniciar.
Retry-AfterPresente nos 429 de janela (rate): espere estes segundos antes de repetir. O 429 teto-leads não o envia — esse limite não reseta com o tempo.

Limites atuais: 60 cotações/min por chave em /v1/quote e 120/min em /v1/coverage. Antes da autenticação há ainda um freio por IP de 300 requisições/min (anti-flood; vale pra REST e MCP). No MCP, o transporte /api/mcp aceita 120 mensagens JSON-RPC/min por chave (initialize, tools/list etc.), mas as ferramentas debitam os mesmos buckets do REST:quote_solar conta nas 60 cotações/min e check_coverage nas 120/min — usar os dois canais não multiplica o limite. Ao estourar no REST, você recebe 429 com Retry-After. No MCP são dois casos: estourar o teto do transporte (ou o freio por IP) devolve HTTP 429 com erro JSON-RPC -32000 e o header Retry-After; estourar o limite de uma ferramenta devolve um resultado de ferramenta com isError e (rate) na mensagem. Há também um teto absoluto de propostas por conta (429 teto-leads) — esse vem sem Retry-After, porque não reseta sozinho: fale com o suporte.

Idempotência

Para requisições que gravam (não-preview), envie um cabeçalho Idempotency-Key único por operação (ex.: o id do pedido no seu sistema). Se a mesma chave chegar de novo com o mesmo corpo — típico de retry por timeout de rede — devolvemos a mesma proposta (com "idempotent": true), sem duplicar o lead.

curl -X POST https://app.ionluz.com.br/api/v1/quote \
  -H "Authorization: Bearer ilz_sua_chave" \
  -H "Idempotency-Key: pedido-8421" \
  -H "Content-Type: application/json" \
  -d '{ "consumoKwhMes": 650, "nome": "Ana", "email": "ana@ex.com", "consent": true }'
# Reenviar com a MESMA Idempotency-Key e o MESMO corpo devolve a MESMA proposta
# (campo "idempotent": true), sem criar um lead duplicado.

Se a mesma Idempotency-Key for reusada com um corpo diferente, respondemos 422 idempotencia. Se duas requisições com a mesma chave chegarem ao mesmo tempo, a segunda recebe 409 em-processamento (retentável) — garantindo que nunca se crie proposta duplicada. A janela de deduplicação é de 15 minutos.

Honestidade importante: a deduplicação vive em memória no servidor e zera em restart/deploy. Um retry que atravesse um deploy pode criar uma segunda proposta. Para operações críticas, confira pelo proposalId devolvido antes de reenviar. No MCP, o mesmo mecanismo existe via argumento idempotencyKey de quote_solar (chaves independentes das do REST).

POST/api/v1/quote

Gera um orçamento a partir do consumo. Campos do corpo (JSON):

CampoTipoDescrição
consumoKwhMesnumberObrigatório. Consumo médio mensal em kWh.
ufstringUF (2 letras). Ajusta a irradiação (HSP). Padrão SP.
fasesstringmono, bi ou tri (aceitamos também monofasico/bifasico/trifasico, com ou sem acento, qualquer caixa). Padrão mono. Qualquer outro valor → 422 fase-invalida.
kwpnumberPotência desejada. Se omitido, dimensionamos pelo consumo.
tarifanumberTarifa em R$/kWh. Se omitida, assumimos um padrão (veja assumptions).
nome, email, telefone, cidadestringDados do lead (opcionais; só usados ao gravar).
telhadostringOpcional: ceramico · metalico · fibrocimento · laje · solo · carport. Na cotação por combinação automática, aplica o preço global de estrutura configurado na aba API (por telhado × nº de módulos). Omisso usa o telhado padrão da conta. A resposta indica em assumptions.telhado quando entrou na conta.
previewbooleantrue = só simula (não grava, não gera PDF). Padrão false.
consentbooleanObrigatório ao gravar (preview: false): true confirma que o titular consentiu (LGPD) com o registro dos dados. Registramos como prova a data, a versão do texto da política e metadados técnicos da requisição (IP, user-agent e origem, quando presentes). Sem ele, 400 consentimento-obrigatorio. Em preview, é ignorado.

Resposta — modo simulação (preview: true)

{
  "ok": true,
  "orcamento": {
    "potenciaKwp": 5.4,
    "valorFinal": 28500,
    "paybackAnos": 6.3,
    "economiaMes1": 336,
    "economia25Anos": 216247,
    "sePaga": true
  },
  "assumptions": {
    "uf": "SP", "fase": "bi", "hspHoraDia": 5.2,
    "tarifaBrlKwh": 0.95, "tarifaAssumida": true,
    "kwpAlvo": 5.28, "kwpAlvoAssumido": true
  },
  "preview": true,
  "aviso": "Estimativa sujeita a visita técnica.",
  "requestId": "req_9f2c1a7b3d4e5f6a90"
}

O bloco assumptions revela o que o motor assumiu quando você omitiu algo — por exemplo,tarifaAssumida: true significa que caímos na tarifa padrão porque você não enviou tarifa. Use isso para deixar claro ao usuário final o que foi estimado.

Deslocamento por km (opcional): se você configurar a sua cidade-sede no perfil, cotações para clientes em outra cidade recebem um acréscimo de R$/km (padrão R$ 5,00, editável) pela distância rodoviária estimada entre as sedes municipais (IBGE, fator rodoviário 1,35). O acréscimo já entra no valorFinal antes do cálculo — payback e economia saem coerentes com o valor cobrado — e a resposta ganha o bloco deslocamento: { "km": 87, "valorBrl": 435 }para o seu controle (ele não aparece no documento do cliente). Mesma cidade não altera o preço. Cidade do cliente não reconhecida: no mesmo estado, sem acréscimo; em outro estado, cobra até o centro do estado.

Resposta — modo gravação (preview: false, exige consent: true)

{
  "ok": true,
  "orcamento": { "potenciaKwp": 5.4, "valorFinal": 28500, "paybackAnos": 6.3,
    "economiaMes1": 336, "economia25Anos": 216247, "sePaga": true },
  "assumptions": { "uf": "SP", "fase": "bi", "hspHoraDia": 5.2,
    "tarifaBrlKwh": 0.95, "tarifaAssumida": true, "kwpAlvo": 5.28, "kwpAlvoAssumido": true },
  "proposalId": "clx8h2k9a0001abcd",
  "pdfUrl": "https://app.ionluz.com.br/api/pub/clx8h2k9a0001abcd/pdf?t=...",
  "propostaUrl": "https://app.ionluz.com.br/p/clx8h2k9a0001abcd?t=...",
  "aviso": "Estimativa sujeita a visita técnica.",
  "requestId": "req_9f2c1a7b3d4e5f6a90"
}

Ao gravar, você recebe proposalId, o link do pdfUrl (PDF pronto, com a sua marca) e opropostaUrl (página interativa que o cliente abre no celular — antes/depois da conta, gráfico de payback e financiamento). Ambos os links já vêm assinados e são públicos para compartilhar.

Exemplos

// Node.js / navegador (fetch nativo). Use preview:true p/ simular sem gravar.
async function cotar(consumoKwhMes, uf = "SP") {
  const res = await fetch("https://app.ionluz.com.br/api/v1/quote", {
    method: "POST",
    headers: {
      "Authorization": "Bearer " + process.env.IONLUZ_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ consumoKwhMes, uf, preview: true }),
  });
  const data = await res.json();
  if (!data.ok) throw new Error(data.error + ": " + data.message);
  return data.orcamento;
}
import os, requests

def cotar(consumo_kwh_mes, uf="SP", persist=False):
    corpo = {"consumoKwhMes": consumo_kwh_mes, "uf": uf, "preview": not persist}
    if persist:
        corpo["consent"] = True  # obrigatório ao gravar: o titular consentiu (LGPD)
    r = requests.post(
        "https://app.ionluz.com.br/api/v1/quote",
        headers={"Authorization": f"Bearer {os.environ['IONLUZ_API_KEY']}"},
        json=corpo,
        timeout=30,
    )
    data = r.json()
    if not data["ok"]:
        raise RuntimeError(f"{data['error']}: {data['message']}")
    return data["orcamento"]

GET/api/v1/coverage

Descobre quais faixas × fase você atende antes de cotar — assim seu sistema (ou agente) evita o errofaixa-nao-atendida em runtime. Mesma autenticação por chave.

curl https://app.ionluz.com.br/api/v1/coverage -H "Authorization: Bearer ilz_sua_chave"
{
  "ok": true,
  "modo": "custo",
  "ativa": true,
  "cobertas": 5,
  "total": 6,
  "tetoKw": 75,
  "unidadeFaixas": "kw-saida-inversor",
  "fases": [ { "key": "mono", "label": "Monofásico" }, ... ],
  "matriz": [
    { "fase": "mono", "deKw": 0,  "ateKw": 3,  "atende": true },
    { "fase": "mono", "deKw": 3,  "ateKw": 10, "atende": true },
    { "fase": "tri",  "deKw": 0,  "ateKw": 75, "atende": false }
  ],
  "requestId": "req_..."
}

As faixas são do integrador (editáveis — a matriz pode mudar) e são denominadas em kW de saída de inversor (CA), na régua 0–tetoKw (75 kW, o limite da microgeração). Cada célula de matriz vai de deKw a ateKw e diz se aquela combinação com fase tem um kit precificável (atende: true). Fase com combinação automática aparece como uma faixa única 0–75.

Erros

Erros têm sempre o mesmo formato: ok: false, um error estável (para o seu código), uma message humana e o requestId.

{
  "ok": false,
  "error": "faixa-nao-atendida",
  "message": "Nenhum kit padrão precificável cobre essa faixa de potência para essa fase. Verifique a cobertura da sua API.",
  "docs": "https://app.ionluz.com.br/docs/api",
  "requestId": "req_9f2c1a7b3d4e5f6a90",
  "kwp": 12.3,
  "fase": "tri"
}
errorHTTPO que fazer
chave-invalida401Confira o header Authorization: Bearer ilz_...; a chave pode ter sido revogada.
json400Corpo não é JSON válido. Verifique o Content-Type: application/json.
payload-grande413Corpo passou de 4 KB.
consumo400consumoKwhMes ausente ou ≤ 0.
consumo-fora-de-faixa400consumoKwhMes acima do plausível (1.000.000 kWh/mês). Confira a unidade — é kWh por mês, não por ano.
fase-invalida422fases não reconhecida. Envie mono, bi ou tri (ou monofasico/bifasico/trifasico, com ou sem acento).
consentimento-obrigatorio400Gravação (não-preview) sem consent: true. Colete o consentimento do titular e reenvie.
plano-sem-api403Chave válida, mas o plano da conta não inclui API — a API pública é do plano Total + API (R$97/mês). Assine em /plano.
metodo-nao-permitido405Método HTTP errado (ex.: GET em /v1/quote, POST em /v1/coverage). A resposta indica o método certo.
faixa-nao-atendida422Não há kit para essa faixa/fase. Consulte /v1/coverage.
sem-preco-configurado422Sua formação de preço não gerou valor. Revise Preços no painel.
idempotencia422Idempotency-Key reutilizada com corpo diferente. Use uma chave nova.
em-processamento409Outra requisição com a mesma Idempotency-Key ainda está processando. Repita em instantes.
rate429Muitas requisições. Espere o Retry-After.
teto-leads429Limite absoluto de propostas da conta atingido. Vem sem Retry-After — não adianta repetir; fale com o suporte.
interno500Erro nosso. Repita; se persistir, contate o suporte com o requestId.

MCP — para agentes de IA

O Model Context Protocol deixa um agente de IA (Claude, ChatGPT via conectores, ou o seu próprio) usar a ionluz como ferramenta. Aponte o cliente MCP para https://app.ionluz.com.br/api/mcp com a sua chave e o agente ganha duas ferramentas prontas — sem você escrever integração alguma.

Configuração (clientes com MCP remoto/HTTP)

{
  "mcpServers": {
    "ionluz": {
      "url": "https://app.ionluz.com.br/api/mcp",
      "headers": { "Authorization": "Bearer ilz_sua_chave" }
    }
  }
}

Ferramentas expostas

// tools/list devolve:
[
  { "name": "quote_solar",   "description": "Gera um orçamento de energia solar a partir do consumo..." },
  { "name": "check_coverage","description": "Lista quais faixas (kW de saída de inversor) × fase o integrador atende..." }
]
  • quote_solar — cota a partir do consumo. Por padrão simula; passe persist: true para gravar a proposta e receber os links. Ao gravar, consent: true é obrigatório (consentimento LGPD do titular — sem ele a ferramenta devolve o erro consentimento-obrigatorio).
  • idempotencyKey (argumento opcional de quote_solar, só com persist) — mesma garantia da Idempotency-Key do REST: repetir a mesma chave com os mesmos argumentos devolve a mesma proposta(idempotent: true no structuredContent), sem duplicar o lead; duas chamadas simultâneas nunca criam duas. As chaves são independentes das do REST. Sem idempotencyKey, não há deduplicação — retries podem duplicar.
  • check_coverage — lista as faixas atendidas, para o agente cotar só o que existe.

O transporte é Streamable HTTP (JSON-RPC 2.0). A negociação (initialize), a listagem (tools/list) e a execução (tools/call) seguem o protocolo padrão — qualquer cliente MCP compatível conecta. Falhas internas chegam como erro JSON-RPC estruturado (-32603, erro-interno) ou como resultado de ferramenta com isError — nunca um 500 sem envelope.

A mesma lógica e o mesmo preço da API REST valem no MCP: o agente devolve exatamente o que o seu /v1/quote devolveria — e consome os mesmos limites (ver Limites & headers).

Calculadora no seu site

O modo preview foi feito para calculadoras públicas: cotar sem poluir seus leads. Mas não coloque a chave no navegador — qualquer visitante a leria. O padrão seguro é um proxy no seu backend que injeta a chave:

<!-- Calculadora no seu site — NUNCA exponha a chave no navegador.
     Chame o seu backend, e o SEU backend chama a ionluz com a chave. -->
<form id="calc">
  <input name="consumo" type="number" placeholder="Consumo mensal (kWh)" required />
  <select name="uf"><option>SP</option><option>MG</option><option>RJ</option></select>
  <button>Simular economia</button>
</form>
<p id="resultado"></p>
<script>
document.getElementById("calc").onsubmit = async (e) => {
  e.preventDefault();
  const f = e.target;
  // /minha-cotacao é uma rota SUA que injeta a chave e repassa p/ a ionluz (preview)
  const r = await fetch("/minha-cotacao", {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ consumoKwhMes: +f.consumo.value, uf: f.uf.value }),
  });
  const { orcamento } = await r.json();
  document.getElementById("resultado").textContent =
    "Sistema de " + orcamento.potenciaKwp + " kWp — economia de R$ " +
    orcamento.economiaMes1 + "/mês.";
};
</script>

O seu endpoint /minha-cotacao recebe o consumo, chama https://app.ionluz.com.br/api/v1/quote com a chave (server-side, preview: true) e repassa só o orcamento ao navegador.

Boas práticas

  • Segurança: chave só no servidor. Rotacione ao suspeitar de vazamento (revogar + gerar nova é instantâneo).
  • Simule antes de gravar: preview: true em calculadoras e testes; grave só quando virar lead de verdade.
  • Trate os assumptions: mostre ao usuário quando a tarifa/potência foi estimada, para transparência.
  • Consentimento primeiro: ao gravar, colete o aceite do titular e envie consent: true — é obrigatório e fica registrado como prova (LGPD).
  • Retries com idempotência: em POSTs que gravam, sempre envie Idempotency-Key (no MCP, o argumento idempotencyKey).
  • Respeite o rate-limit: use os headers X-RateLimit-* e recue no 429.
  • Cobertura primeiro: consulte /v1/coverage (ou check_coverage) para não cotar faixas que você não atende.
  • Estimativas: todo valor é sujeito a visita técnica — deixe isso visível ao cliente final.
API v1 · ionluz — orçamentos solares. Dúvidas? Cite o requestId ao contatar o suporte.
API & MCP — ionluz para desenvolvedores