API v1
Conecta tu bot de WhatsApp, tu CRM o tus automatizaciones (Make, n8n, Zapier) con tu cuenta de Zenit. Tu cliente manda la foto de su recibo CFE; Zenit la lee con el mismo motor del cotizador, dimensiona el sistema y deja el borrador de cotización con tus planes y precios.
1. Crea una clave. En Zenit, un administrador entra a Configuración → API para integraciones y crea una clave (planes Pro y Master). Se muestra una sola vez: guárdala en tu servidor como ZENIT_API_KEY.
2. Pruébala. La respuesta dice tu organización, el usuario con el que actúa la clave y cuántas lecturas te quedan hoy.
curl https://zenitmaster.com/api/v1/me \
-H "Authorization: Bearer $ZENIT_API_KEY"3. Lee un recibo. Un recibo por solicitud: el PDF, o de 1 a 4 fotos de sus páginas. Con Prefer: wait=90 la respuesta espera la lectura terminada; sin él responde 202 al instante y la consultas en GET /bill-readings/{id} o recibes el webhook.
curl https://zenitmaster.com/api/v1/bill-readings \
-H "Authorization: Bearer $ZENIT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Prefer: wait=90" \
-F "file=@recibo-frente.jpg" \
-F "file=@recibo-reverso.jpg" \
-F "reference=whatsapp:5218100000000"La lectura trae el titular, número de servicio, tarifa, historial, consumo y gasto anual, y el sistema sugerido (kWp, paneles, producción). Revisa warnings: history_missing significa que falta la página del historial — pide el reverso y vuelve a enviar todas las fotos juntas.
4. Cotiza. Crea el borrador con tus planes, catálogo y precios; el recibo queda adjunto y tu equipo lo ve en Cotizaciones.
curl https://zenitmaster.com/api/v1/quotations \
-H "Authorization: Bearer $ZENIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"bill_reading_id": "5b0f0c4e-2f7a-4a52-9a43-0d3f6f1c2a11",
"client": { "phone": "+5218100000000" },
"lead_source": "Facebook"
}'5. (Opcional) Compártelo. Con una clave que tenga el permiso Compartir propuestas, publica la propuesta y envía al cliente share.url (y el PDF en share.pdf_url).
curl -X POST https://zenitmaster.com/api/v1/quotations/<id>/share \
-H "Authorization: Bearer $ZENIT_API_KEY"El patrón: cuando un chat te manda fotos o un PDF, agrúpalas durante ~1 minuto (la gente manda el frente y luego el reverso), léelas como un recibo y responde con el resultado. Si el cliente manda varios recibos (varias casas o negocios), haz una lectura por recibo y cotiza cada una; guarda client.id por chat y envíalo como client_id para que todas las cotizaciones queden en el mismo cliente.
// Webhook de tu bot (Meta WhatsApp Cloud API), Node 20+.
const GRAPH = "https://graph.facebook.com/v23.0"; // la versión de Graph API de tu app
const ZENIT = "https://zenitmaster.com/api/v1";
const zenitHeaders = { Authorization: `Bearer ${process.env.ZENIT_API_KEY}` };
// 1) Descarga la foto (o el PDF) que te mandó el cliente.
async function downloadMedia(mediaId) {
const auth = { Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}` };
const media = await fetch(`${GRAPH}/${mediaId}`, { headers: auth }).then((r) => r.json());
return fetch(media.url, { headers: auth }).then((r) => r.blob());
}
// 2) Lee el recibo: todas las fotos que el cliente mandó juntas (frente y reverso).
async function readBill(chatId, messages) {
const form = new FormData();
for (const m of messages) {
const media = m.image ?? m.document;
form.append("file", await downloadMedia(media.id), m.document?.filename ?? "recibo.jpg");
}
form.append("reference", `whatsapp:${chatId}`);
const res = await fetch(`${ZENIT}/bill-readings`, {
method: "POST",
// El id del último mensaje: si Meta reenvía el webhook, Zenit no lee dos veces.
headers: { ...zenitHeaders, "Idempotency-Key": messages.at(-1).id, Prefer: "wait=90" },
body: form,
});
return res.json(); // status: "succeeded" | "failed" | "processing" (→ espera el webhook)
}
// 3) Cotiza con tus planes y precios.
async function quote(reading, chatId, clientId) {
const res = await fetch(`${ZENIT}/quotations`, {
method: "POST",
headers: { ...zenitHeaders, "Content-Type": "application/json", "Idempotency-Key": `quote-${reading.id}` },
body: JSON.stringify({
bill_reading_id: reading.id,
...(clientId ? { client_id: clientId } : { client: { phone: `+${chatId}` } }),
}),
});
return res.json(); // guarda quotation.client.id para los siguientes recibos de este chat
}Twilio: los archivos llegan como MediaUrl0, MediaUrl1… Si tu cuenta no exige autenticación para los medios, envíalos tal cual: { "files": [{ "url": MediaUrl0 }] } en JSON. Si la exige, descárgalos con tu Account SID y Auth Token y súbelos en multipart.
ManyChat, Make, n8n y similares: usa JSON con las URLs de las fotos y Prefer: wait=90 si la plataforma espera ese tiempo; si corta antes (muchas cortan a los 10–30 s), toma el id del 202 y consulta la lectura en un paso posterior, o recibe el webhook.
Configura una URL https en tu clave (Configuración → API → Webhook). Al terminar cada lectura Zenit envía bill_reading.succeeded o bill_reading.failed con la lectura completa en data.object, firmada con el secreto del webhook en el encabezado Zenit-Signature: t=…,v1=….
import crypto from "node:crypto";
// rawBody: el cuerpo EXACTO que recibiste (sin volver a serializar el JSON).
export function verifyZenitSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"),
);
const received = Buffer.from(parts.v1 ?? "");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}Responde 2xx en menos de 10 s (procesa después). Reintentamos hasta 3 veces; un evento puede llegar más de una vez, así que descarta los repetidos por id. La fuente de verdad sigue siendo GET /bill-readings/{id}.
Todo error tiene la forma { "error": { "code", "message", "param", "request_id" } }. Programa contra code; message está en español y se puede mostrar al usuario. Incluye request_id (también en el encabezado X-Request-Id) si escribes a soporte.
| HTTP | code | Qué hacer |
|---|---|---|
| 401 | missing_api_key · invalid_api_key · revoked_api_key | Revisa el encabezado Authorization o crea otra clave. |
| 403 | plan_without_api · subscription_inactive | La API requiere plan Pro o Master activo. |
| 403 | missing_permission | La clave no tiene el permiso (p.ej. «Compartir propuestas»). |
| 400 | no_files · too_many_files · file_too_large · unsupported_file_type · heic_not_supported | Envía el PDF o de 1 a 4 fotos JPG, PNG o WebP de hasta 10 MB. |
| 400 | invalid_request · invalid_json · invalid_file_url | El cuerpo no cumple el esquema; «param» dice qué campo. |
| 409 | idempotency_key_reused · idempotency_key_in_progress | Usa una Idempotency-Key nueva por operación; reintenta tras Retry-After. |
| 409 | bill_reading_not_ready · bill_reading_failed | Cotiza cuando la lectura tenga status «succeeded». |
| 429 | daily_limit_reached · too_many_in_flight | Espera lo que indica Retry-After. |
Una lectura que termina en failed trae su motivo en error.code:
unreadable_bill — No se leyó el consumo. Pide al cliente el PDF o fotos nítidas de ambas páginas.file_download_failed — No se pudo descargar la URL (¿requiere autenticación?). Envía el archivo en multipart o en «data».processing_timeout — La lectura no terminó a tiempo. Envía el recibo de nuevo.Límites: 200 lecturas de recibo por organización en 24 horas y 10 leyéndose a la vez. Las cotizaciones cuentan para el límite mensual de tu plan. Archivos de hasta 10 MB; el cuerpo de una solicitud, hasta 4 MB (para archivos más grandes envía una URL).
Reintentos: envía Idempotency-Key en cada POST (un UUID, o el id del mensaje de WhatsApp). Reintentar con la misma clave devuelve la primera respuesta: no se lee el recibo dos veces ni se crea otro cliente.
La clave actúa como el administrador que la creó: aplican su rol y sus permisos, y si deja la organización la clave deja de funcionar. Guárdala solo en tu servidor — nunca en una app, en el navegador ni en una URL — y revócala en Configuración → API si se filtra.
Los recibos contienen datos personales de tus clientes. Tú decides qué recibos envías y eres responsable de avisarles cómo los usas; Zenit los procesa por cuenta tuya y los guarda en tu cuenta, igual que los recibos que subes en el cotizador. Consulta el aviso de privacidad.
Esta es la v1 (https://zenitmaster.com/api/v1). Agregar campos, endpoints o códigos de error no cambia la versión: ignora los campos que no conozcas. Un cambio incompatible será /api/v2, anunciado aquí con al menos 6 meses de anticipación y con los encabezados Deprecation y Sunset en las respuestas de la versión anterior.