← Volver al inicio

API v1

API de Zenit

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.

Inicio rápido

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"

Receta: bot de WhatsApp

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.

Webhooks

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}.

Errores y límites

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.

HTTPcodeQué hacer
401missing_api_key · invalid_api_key · revoked_api_keyRevisa el encabezado Authorization o crea otra clave.
403plan_without_api · subscription_inactiveLa API requiere plan Pro o Master activo.
403missing_permissionLa clave no tiene el permiso (p.ej. «Compartir propuestas»).
400no_files · too_many_files · file_too_large · unsupported_file_type · heic_not_supportedEnvía el PDF o de 1 a 4 fotos JPG, PNG o WebP de hasta 10 MB.
400invalid_request · invalid_json · invalid_file_urlEl cuerpo no cumple el esquema; «param» dice qué campo.
409idempotency_key_reused · idempotency_key_in_progressUsa una Idempotency-Key nueva por operación; reintenta tras Retry-After.
409bill_reading_not_ready · bill_reading_failedCotiza cuando la lectura tenga status «succeeded».
429daily_limit_reached · too_many_in_flightEspera 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.

Seguridad y datos

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.

Versiones y cambios

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.

  • 15 oct 2026 — v1. Lectura de recibos CFE (PDF, fotos o URLs), cotizaciones con tus planes, enlaces para el cliente, webhooks firmados e Idempotency-Key.