API & MCP
Cotação
POST/api/v1/quote
Gera um orçamento a partir do consumo ou do valor da conta de luz. Campos do corpo (JSON, até 4 KB):
| Campo | Tipo | Descrição |
|---|---|---|
consumoKwhMes | number | Obrigatório, ou valorContaBrl. Consumo médio mensal em kWh, maior que zero. Fora de uma faixa plausível: 400 consumo-fora-de-faixa. Com os dois campos, vale o consumo. |
valorContaBrl | number | O valor total de uma conta de luz, em reais, para quando o cliente não sabe o kWh. O consumo sai estimado pela tarifa e a resposta traz consumoEstimado: true. Veja Pelo valor da conta de luz. |
uf | string | UF de 2 letras. Define as horas de sol (a média do estado, quando a cidade não casa), a tarifa padrão e o estado do cliente para o deslocamento e o raio de atendimento. Padrão SP. UF desconhecida vale a média nacional (hspFonte: "fallback"). |
cidade | string | Recomendado. Quando o nome casa com uma das 100 cidades do Atlas Brasileiro de Energia Solar, as horas de sol são as do município (hspFonte: "cidade-atlas"). Também decide o raio de atendimento e o deslocamento da conta. Mande já na simulação: sem ela, o número pode mudar na hora de gravar. |
fases | string | mono, bi ou tri (aceitamos também monofasico, bifasico e trifasico, com ou sem acento, em qualquer caixa). Padrão mono. Outro valor: 422 fase-invalida. |
kwp | number | Potência desejada. Sem ela, dimensionamos pelo consumo menos a taxa mínima da distribuidora (30, 50 ou 100 kWh, pela ligação). É um alvo: o sistema entregue é o do kit ou da composição mais próxima, e cobrePct diz quanto ele cobre. |
tarifa | number | Tarifa em R$/kWh. Sem ela, vale a tarifa residencial B1 da UF homologada pela ANEEL (SP: 0,99), a mesma das páginas por cidade do site. O valor informado fica entre 0,01 e 10. |
telhado | string | ceramico, metalico, fibrocimento, laje, solo ou carport. Escolhe a estrutura das cotações montadas por combinação automática ou pelo catálogo padrão. Muda o preço só no modo Custo → venda (nos outros, o preço sai da tabela por kWp e a estrutura vai como item do documento). Sem ele, o telhado padrão da conta. assumptions.telhado diz quando a estrutura entrou na conta. |
nome, email, telefone | string | Dados do lead, usados ao gravar. E-mail fora do formato e telefone com menos de 8 dígitos são descartados. |
preview | boolean | true só simula: não grava, não gera PDF. Padrão false. Aceita também "true" ou "1", e o parâmetro ?preview=true. |
consent | boolean | Obrigatório ao gravar (preview ausente ou false): true confirma que o titular consentiu com o registro dos dados (LGPD). Registramos como prova a data, a versão do texto da política e metadados da requisição (IP, user-agent e origem, quando presentes). Sem ele, 400 consentimento-obrigatorio. Em preview é ignorado. |
opcao | string | só no motor novo Qual opção vira a proposta: economica, recomendada ou bateria. Valor fora da lista: 400 opcao-invalida. Nas outras contas o campo é ignorado. Veja Contas com o motor novo. |
Resposta, modo simulação (preview: true)
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 }'{
"ok": true,
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": 14296.8,
"paybackAnos": 2.057067267830044,
"economiaMes1": 569,
"economia25Anos": 373181,
"sePaga": true,
"cobrePct": 111
},
"assumptions": {
"uf": "SP",
"fase": "bi",
"hspHoraDia": 4.9,
"hspFonte": "uf-media",
"tarifaBrlKwh": 0.99,
"tarifaAssumida": true,
"kwpAlvo": 5.16,
"kwpAlvoAssumido": true,
"kitCatalogoPadrao": true
},
"preview": true,
"aviso": "Estimativa sujeita a visita técnica.",
"requestId": "req_9f2c1a7b3d4e5f6a90"
}Conta que ainda cota pela referência de mercado (R$ 3.200 por obra + R$ 1.940 por kWp) e sem kit próprio na faixa. Com a sua tabela e os seus kits, os números mudam.
O bloco assumptions diz o que o motor assumiu quando você omitiu algo. tarifaAssumida: true: valeu a tarifa padrão da UF. hspFonte: cidade-atlas (horas de sol do município, com hspCidade), uf-media (média do estado) ou fallback (UF desconhecida, média nacional). kitCatalogoPadrao: true: a faixa não tinha kit seu e o equipamento veio do catálogo padrão ionluz. precoReferencia: true: a conta não tinha preço utilizável e o valor saiu da referência de mercado do catálogo. kitMultiplo: quantas unidades do mesmo kit foram somadas para chegar perto do alvo. Mostre ao cliente o que foi estimado.
orcamento.cobrePct vem em toda cotação: quanto do consumo o sistema cobre, em % da potência-alvo, que mira o consumo menos a taxa mínima da distribuidora (30, 50 ou 100 kWh, pela ligação). 100 é o alvo cheio; abaixo disso o sistema gera menos que o consumo.
Pelo valor da conta de luz (valorContaBrl)
Quem vai instalar em casa sabe quanto paga de luz e quase nunca sabe o consumo em kWh. Mande valorContaBrl, o valor total de uma conta, em reais, no lugar de consumoKwhMes. O consumo é estimado assim:
consumo estimado (kWh por mês) = valorContaBrl ÷ tarifa, arredondado ao kWh- A tarifa é a do campo
tarifaou, sem ele, a residencial B1 da UF (SP: 0,99), a mesma que entra no payback. Assim a economia do orçamento conversa com o valor que o cliente informou. Exemplo: R$ 500 em SP dão 500 ÷ 0,99 = 505 kWh por mês. - A taxa mínima da distribuidora (30, 50 ou 100 kWh, pela ligação) não sai do valor: sem energia solar, a conta cobra o maior entre o consumo e a taxa mínima, e o dimensionamento já desconta a taxa mínima. Conta que não passa dela responde
422 valor-conta-na-taxa-minima, comtaxaMinimaKwh,taxaMinimaBrl,tarifaBrlKwhefase: não há consumo para a energia solar compensar. - Valor que não é um número maior que zero:
400 valor-conta. Consumo estimado acima do plausível:400 valor-conta-fora-de-faixa. - Com
consumoKwhMesevalorContaBrljuntos, vale o consumo. Consumo zero ou vazio junto do valor conta como não informado.
A resposta traz, no topo, consumoEstimado: true e o consumoKwhMes usado; o resto é o de sempre. Diga ao cliente que o consumo foi estimado pelo valor da conta e peça a conta de luz para o número exato: iluminação pública, bandeira tarifária e a diferença entre a distribuidora dele e a de referência do estado ficam de fora da estimativa. Ao gravar, a proposta guarda o valor e a tarifa usados, e o webhook lead.criado traz consumoEstimado e valorContaBrl.
// pedido
{ "valorContaBrl": 500, "uf": "SP", "fases": "bi", "cidade": "Sorocaba", "preview": true }
// resposta: os campos de sempre, mais estes dois
{ "consumoEstimado": true, "consumoKwhMes": 505 }Deslocamento e área de atendimento
Em Perfil e marca, aba Condições, bloco "Área de atendimento e deslocamento", a conta define a cidade-sede, um raio de atendimento em km e um valor por km.
- Só cobra quem preencheu um valor por km maior que zero; em branco ou zero, nenhuma cotação ganha acréscimo.
- A distância é a rodoviária estimada entre as sedes municipais (IBGE, a linha reta vezes 1,35). Mesma cidade não cobra. Cidade não reconhecida: no mesmo estado, sem acréscimo; em outro estado, a distância vai até o centro do estado.
- Sem raio, o deslocamento vale para todo cliente fora da cidade-sede. Com raio, dentro dele o preço é o da tabela, sem deslocamento. Acima dele, conforme a escolha da conta:
422 fora-da-area(o padrão; o pedido vira contato) ou a cotação com o deslocamento da viagem inteira. - Quando há cobrança, ela entra no
valorFinalantes do cálculo do payback e vira uma linha visível na proposta.
Conta com sede em Sorocaba cobrando R$ 2 por km, sem raio, e cliente de Campinas:
{
"ok": true,
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": 14502.8,
"paybackAnos": 1.9319543963387549,
"economiaMes1": 617,
"economia25Anos": 396502,
"sePaga": true,
"cobrePct": 97,
"deslocamento": {
"km": 103,
"valorBrl": 206,
"destino": "Campinas",
"descricao": "Deslocamento técnico até Campinas: 103 km, R$ 206 incluídos no valor"
}
},
"assumptions": {
"uf": "SP",
"fase": "bi",
"hspHoraDia": 5.2,
"hspFonte": "cidade-atlas",
"hspCidade": "Campinas",
"tarifaBrlKwh": 0.99,
"tarifaAssumida": true,
"kwpAlvo": 5.91,
"kwpAlvoAssumido": true,
"kitCatalogoPadrao": true
},
"deslocamento": { "km": 103, "valorBrl": 206, "destino": "Campinas" },
"...": "..."
}O deslocamento de fora do orcamento é o mesmo, sem a descricao.
A mesma conta com um raio de 50 km e "pedir contato" acima dele:
{
"ok": false,
"error": "fora-da-area",
"message": "O cliente está acima do raio de atendimento do perfil, e a conta pede contato em vez de preço fora do raio. A resposta traz distanciaKm, raioKm e destino.",
"docs": "https://app.ionluz.com.br/docs/api",
"requestId": "req_9f2c1a7b3d4e5f6a90",
"kwp": 5.91,
"fase": "bi",
"distanciaKm": 103,
"raioKm": 50,
"destino": "Campinas"
}Com "cotar e cobrar o deslocamento" no lugar de "pedir contato", o mesmo pedido volta cotado com os 103 km inteiros: R$ 206 na linha de deslocamento.
Resposta, modo gravação (preview: false, exige consent: true)
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": 620, "uf": "SP", "cidade": "Sorocaba", "fases": "bi", "nome": "Cliente Exemplo", "email": "cliente@exemplo.com.br", "telefone": "(15) 99999-0000", "consent": true }'{
"ok": true,
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": 14296.8,
"paybackAnos": 2.1406530441031313,
"economiaMes1": 545,
"economia25Anos": 375409,
"sePaga": true,
"cobrePct": 120
},
"assumptions": {
"uf": "SP",
"fase": "bi",
"hspHoraDia": 5.05,
"hspFonte": "cidade-atlas",
"hspCidade": "Sorocaba",
"tarifaBrlKwh": 0.99,
"tarifaAssumida": true,
"kwpAlvo": 4.75,
"kwpAlvoAssumido": true,
"kitCatalogoPadrao": 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 o proposalId, o pdfUrl (PDF pronto, com a sua marca) e o propostaUrl (a página interativa que o cliente abre no celular, com antes e depois da conta, gráfico de payback e, se você ligou, financiamento). Os dois links já vêm assinados e podem ir ao cliente. A proposta aparece em Propostas com a origem "API", vale 15 dias (ou o que você definiu no perfil) e dispara o webhook lead.criado. Reenvio com a mesma Idempotency-Key devolve esta mesma resposta com "idempotent": true.
Contas com o motor novo
O ionluz está ligando um motor novo de cotação conta a conta. Nessas contas a resposta mantém todos os campos acima e ganha outros; nenhum campo muda de nome. Para saber se a conta que você integra já responde assim, olhe a resposta: ela traz sistema e preco.publicado. Programe para as duas situações e a integração não quebra quando a conta mudar.
O preço ao cliente segue a publicação da conta. A conta escolhe em Sua página, cartão "Como a página mostra o preço", e a escolha vale para a página, a API e o MCP:
- Automático (recomendado): o valor exato quando a tabela é da conta; uma faixa de 5% para baixo e para cima, com as pontas arredondadas para fora em R$ 100, enquanto a conta cota pela referência de mercado;
- O valor exato: o valor;
- Uma faixa de valor: "entre R$ X e R$ Y", de 1% a 25%, com o valor final confirmado na visita técnica;
- Só o pedido, sem preço: nenhum preço; a empresa apresenta o valor em contato;
- e, à parte, Eu aprovo antes: cada pedido espera o OK da empresa, e nenhum preço sai até lá.
Por isso: mostre ao cliente preco.publicado.rotulo (curto) ou preco.publicado.texto (a frase). Em faixa, orcamento.valorFinal é o centro da faixa: não mostre como valor exato. Com "eu aprovo antes" pendente ou "só o pedido", valorFinal e paybackAnos vêm null, o deslocamento vem sem o valor em reais, e o pdfUrl responde 409 até a empresa liberar o valor; o propostaUrl abre e diz ao cliente que o valor está em confirmação ou com a empresa.
// Mostra o preço como a conta publica. Serve para as duas situações:
// conta sem o motor novo (não há preco.publicado) e conta com ele.
function precoParaMostrar(resposta) {
const p = resposta.preco && resposta.preco.publicado;
if (p) return p.rotulo; // "R$ 14.297", "entre R$ 13.500 e R$ 15.100", "com a empresa", "em confirmação"
return "R$ " + Math.round(resposta.orcamento.valorFinal).toLocaleString("pt-BR");
}
// Só os avisos escritos para o cliente final. Os de para: "integrador" são recados para você.
function avisosDoCliente(resposta) {
return (resposta.avisos || []).filter(a => a.para === "cliente").map(a => a.texto);
}O mesmo pedido de 600 kWh do começo, numa conta com o motor novo que ainda cota pela referência (por isso a faixa automática, entre R$ 13.500 e R$ 15.100):
{
"ok": true,
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": 14296.8,
"paybackAnos": 2.208259927284166,
"economiaMes1": 527,
"economia25Anos": 363646,
"sePaga": true,
"cobrePct": 121
},
"assumptions": {
"uf": "SP",
"fase": "bi",
"hspHoraDia": 4.9,
"hspFonte": "uf-media",
"tarifaBrlKwh": 0.99,
"tarifaAssumida": true,
"kwpAlvo": 4.73,
"kwpAlvoAssumido": true,
"kitCatalogoPadrao": true
},
"equipamento": [
{
"kind": "microinverter",
"label": "Hoymiles HMS-2250",
"qty": 2,
"specId": "cmru6jcft0003ta7ilpghaxyf",
"powerW": 2250,
"origem": "equipamento"
},
{
"kind": "module",
"label": "Canadian Solar 715 Wp",
"qty": 8,
"specId": "cmru6jcg8000hta7i49lozqyj",
"powerW": 715,
"origem": "equipamento"
},
{
"kind": "structure",
"label": "Estrutura de fixação para telhado cerâmico",
"qty": 8,
"origem": "regra-da-casa"
},
{
"kind": "monitoring",
"label": "Hoymiles DTU (monitoramento)",
"qty": 1,
"origem": "regra-da-casa"
}
],
"sistema": {
"kwp": 5.72,
"kwpAlvo": 4.73,
"cobrePct": 121,
"modulos": 8,
"saidaKw": 4.5,
"geracaoMesKwh": 665,
"fase": "bi"
},
"validade": { "dias": 15, "ate": "2026-10-10T14:05:00.000Z" },
"avisos": [
{
"codigo": "preco-referencia",
"texto": "Referência de mercado (pesquisa Greener, set/2026): a conta ainda não tem uma tabela de preço própria.",
"para": "integrador"
}
],
"preco": {
"publicado": {
"modo": "faixa",
"valor": 14296.8,
"min": 13500,
"max": 15100,
"pct": 5,
"rotulo": "entre R$ 13.500 e R$ 15.100",
"texto": "Entre R$ 13.500 e R$ 15.100. O valor final é confirmado na visita técnica.",
"nota": "O valor final é confirmado na visita técnica."
}
},
"opcoes": [
{
"chave": "economica",
"nome": "Econômica",
"recomendada": false,
"kwp": 4.29,
"modulos": 6,
"cobrePct": 91,
"preco": { "modo": "faixa", "rotulo": "entre R$ 10.900 e R$ 12.100", "...": "..." },
"...": "..."
},
{
"chave": "recomendada",
"nome": "Recomendada",
"recomendada": true,
"kwp": 5.72,
"modulos": 8,
"cobrePct": 121,
"preco": { "modo": "faixa", "rotulo": "entre R$ 13.500 e R$ 15.100", "...": "..." },
"...": "..."
},
{
"chave": "bateria",
"nome": "Com bateria",
"recomendada": false,
"kwp": 5.72,
"modulos": 8,
"cobrePct": 121,
"bateriaKwh": 10,
"nota": "Sistema híbrido, confirmado em projeto.",
"preco": { "modo": "faixa", "rotulo": "entre R$ 46.800 e R$ 51.800", "...": "..." },
"...": "..."
}
],
"opcao": "recomendada",
"preview": true,
"aviso": "Estimativa sujeita a visita técnica.",
"requestId": "req_9f2c1a7b3d4e5f6a90"
}Os outros modos de preco.publicado, no mesmo pedido:
// conta com o motor novo que escolheu publicar o valor exato
{
"preco": {
"publicado": {
"modo": "exato",
"valor": 14296.8,
"rotulo": "R$ 14.297",
"texto": "R$ 14.297"
}
},
"...": "..."
}
// "só o pedido": valorFinal e paybackAnos vêm null
{
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": null,
"paybackAnos": null,
"economiaMes1": 527,
"economia25Anos": 363646,
"sePaga": true,
"cobrePct": 121
},
"preco": {
"publicado": {
"modo": "contato",
"rotulo": "com a empresa",
"texto": "A Empresa Exemplo Energia Solar apresenta o valor em contato com você."
}
},
"...": "..."
}
// "eu aprovo antes" pendente, com deslocamento cobrado: valorFinal, paybackAnos e o valor do deslocamento vêm null
{
"orcamento": {
"potenciaKwp": 5.72,
"valorFinal": null,
"paybackAnos": null,
"economiaMes1": 617,
"economia25Anos": 396502,
"sePaga": true,
"cobrePct": 97,
"deslocamento": {
"km": 103,
"valorBrl": null,
"destino": "Campinas",
"descricao": "Deslocamento técnico até Campinas: 103 km, incluído no valor"
}
},
"deslocamento": { "km": 103, "valorBrl": null, "destino": "Campinas" },
"preco": {
"publicado": {
"modo": "aprovacao",
"rotulo": "em confirmação",
"texto": "A Empresa Exemplo Energia Solar confirma o valor e te chama."
}
},
"...": "..."
}Campos novos. sistema: kWp entregue, kWp-alvo, cobrePct, módulos, saída CA em kW, geração média mensal do 1º ano e a ligação. equipamento: marca, modelo e quantidade de cada item, com origem (equipamento, extras-da-conta ou regra-da-casa), sem fornecedor e sem custo. validade: os dias do perfil (padrão 15) e a data. avisos: códigos estáveis com texto pronto; para diz a quem o texto se dirige. Mostre ao cliente só os de para: "cliente" (cobre-pouco e gera-mais); os demais (kit-multiplicado, preco-referencia, tabela-sem-preco, custo-desconhecido, preferencia-indisponivel e extras-sem-custo) são para você.
Opções e a escolha do cliente
Com as opções ligadas (o padrão; no mesmo cartão, "Mostrar três opções"), a resposta traz opcoes quando há pelo menos duas: a econômica (a mesma linha de equipamento, o menor sistema que cobre pelo menos 80% do consumo compensável, e só quando sai mais barata), a recomendada (a cotação principal) e a com bateria (a recomendada com a bateria sugerida pelo consumo da noite, sistema híbrido confirmado em projeto; só quando a tabela da conta tem preço de bateria, e a referência de mercado tem). Cada opção traz potência, economia, payback, equipamento e o preço como a conta publica.
Mande em opcao a que o cliente escolheu. Sem o campo, vale a recomendada. A resposta descreve a opção que vale (orcamento, sistema, equipamento e preco.publicado) e diz qual em opcao; em preview é igual, sem gravar. Opção que não veio nesta cotação vira a recomendada, e opcao mostra isso. Valor fora da lista responde 400 opcao-invalida, com parametro e aceito, mesmo quando a conta desligou as opções. A proposta gravada guarda as opções e a escolhida, e a página da proposta mostra a escolhida com o resumo das outras.
// pedido
{ "consumoKwhMes": 600, "uf": "SP", "fases": "bi", "nome": "Cliente Exemplo", "email": "cliente@exemplo.com.br", "telefone": "(15) 99999-0000", "opcao": "bateria", "consent": true }
// resposta: orcamento, sistema, equipamento e preco.publicado são os da opção escolhida
{
"ok": true,
"orcamento": { "potenciaKwp": 5.72, "valorFinal": 49296.8, "...": "..." },
"...": "...",
"equipamento": [
"...",
{
"kind": "battery",
"label": "Bateria de 10 kWh (sistema híbrido, confirmado em projeto)",
"qty": 1,
"powerW": null,
"origem": "equipamento"
}
],
"preco": {
"publicado": { "modo": "faixa", "rotulo": "entre R$ 46.800 e R$ 51.800", "...": "..." }
},
"opcoes": [
{ "chave": "economica", "...": "..." },
{ "chave": "recomendada", "...": "..." },
{ "chave": "bateria", "...": "..." }
],
"opcao": "bateria",
"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"
}Exemplos de código
// Node.js 18+ (fetch nativo), no seu servidor. A chave nunca vai ao navegador.
async function cotar(consumoKwhMes, uf = "SP", cidade) {
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, cidade, preview: true }),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error + ": " + data.message);
return data; // orcamento, assumptions e, nas contas com o motor novo, preco.publicado
}import os, requests
def cotar(consumo_kwh_mes, uf="SP", cidade=None, gravar=False):
corpo = {"consumoKwhMes": consumo_kwh_mes, "uf": uf, "preview": not gravar}
if cidade:
corpo["cidade"] = cidade
if gravar:
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']}")
publicado = (data.get("preco") or {}).get("publicado")
if publicado:
preco = publicado["rotulo"] # o preço como a conta publica
else:
preco = "R$ " + f"{round(data['orcamento']['valorFinal']):,}".replace(",", ".")
return data, precorequestId ao escrever para suporte@ionluz.com.br.