API de Miralotu
Conecta tu sistema (un cotizador, un ERP, un CRM) con Miralotu: crea proyectos y propuestas, publica avances y recibe avisos cuando el cliente acepta, contesta o paga. Miralotu se encarga de la comunicación con el cliente: propuesta web, firma con Face ID o huella, portal con avances, horas y pagos.
URL base: https://miralotu.com/api/v1
Autenticación
Crea una llave en Integraciones (menú del panel, solo el dueño de la cuenta) y envíala en cada petición. Cada llave pertenece a una empresa: solo ve y modifica sus proyectos, con los permisos que elegiste.
curl https://miralotu.com/api/v1/projects \ -H "Authorization: Bearer mlt_…"
| projects:read | Leer proyectos, etapas, avances, pagos, solicitudes y propuestas |
| projects:write | Crear y editar proyectos, etapas, conceptos e hitos |
| updates:write | Publicar avances |
| requests:write | Crear solicitudes al cliente |
| payments:write | Agregar pagos al calendario |
| invitations:write | Invitar personas del cliente |
| proposals:write | Enviar propuestas |
| settings:write | Sincronizar los datos de la empresa (nombre, datos fiscales, contacto y logo) |
| workspaces:write | Dar de alta empresas integradas (solo plan Integrador) |
Convenciones
- Cuerpo y respuestas en JSON. Los errores devuelven
{ "error": "…" }con 400, 401, 403, 404, 409, 422 o 429. - Montos: los que envías van en pesos (p. ej.
85000); los que recibes, en centavos y con el sufijoCents. - Precios unitarios y tarifas: sin IVA. El IVA y las retenciones se calculan con
taxRate,applyRetentions,isrRetentionRateeivaRetentionRate. - Fechas sin hora:
AAAA-MM-DD. Fechas con hora: ISO 8601 (UTC). - Un proyecto se identifica por su
slugo por tu referencia:/projects/ext:CP-2026-0001. - Límites: 120 peticiones por minuto por llave y, en la suscripción normal (un negocio), 2,000 llamadas al mes por cuenta. Para conectar a varios negocios existe el plan Integrador (sin límite mensual; cada negocio es una empresa integrada).
Proyectos
/projectsprojects:readListar proyectos
?externalRef= Filtra por la referencia del sistema externo (p. ej. el folio de tu cotización).
{
"projects": [
{
"id": "3a87e7ad-95ba-465d-a820-2bebdd02f0fc",
"slug": "aceros-del-norte-k3m9x",
"externalRef": "CP-2026-0001",
"name": "Nave industrial",
"clientName": "Aceros del Norte",
"status": "draft",
"quoteType": "items",
"paymentDays": 5,
"currency": "MXN",
"totalCents": 14036000,
"portalUrl": "https://miralotu.com/p/aceros-del-norte-k3m9x",
"progress": 0,
"hoursUsed": 0,
"paidCents": 0
}
]
}/projectsprojects:writeCrear proyecto
Por conceptos (`quoteType: "items"`) con `items` y `milestones`, o por horas (`quoteType: "hours"`) con `hoursPerMonth`, `months` y `hourlyRate`. `externalRef` es tu identificador (único por empresa): con él puedes consultar el proyecto como `/projects/ext:{externalRef}`. `managedBy` (p. ej. `"Picoi"`) indica que tu sistema administra avances, etapas y cobros: en el panel de Miralotu se ven pero no se editan.
Cuerpo{
"externalRef": "CP-2026-0001",
"managedBy": "Picoi",
"name": "Nave industrial",
"clientName": "Aceros del Norte",
"description": "Instalación eléctrica de la nave 2.",
"quoteType": "items",
"taxRate": 16,
"paymentDays": 5,
"startDate": "2026-10-05",
"clientContactName": "Jorge Ramírez",
"clientEmail": "compras@aceros.mx",
"items": [
{
"description": "Instalación eléctrica tablero principal",
"quantity": 1,
"unit": "lote",
"unitPrice": 85000
},
{
"description": "Cableado y canalización",
"quantity": 300,
"unit": "m",
"unitPrice": 120
}
],
"discounts": [
{
"label": "Descuento cliente fundador",
"type": "fixed",
"value": 5000
}
],
"milestones": [
{
"label": "Anticipo",
"trigger": "acceptance",
"amountType": "percent",
"percent": 50
},
{
"label": "Al 50 % de avance",
"trigger": "progress",
"triggerProgress": 50,
"amountType": "percent",
"percent": 30
},
{
"label": "Contra entrega",
"trigger": "delivery",
"amountType": "percent",
"percent": 20
}
],
"stages": [
{
"name": "Ingeniería",
"description": "Planos y memoria de cálculo",
"startDate": "2026-10-05",
"endDate": "2026-10-20"
}
]
}Respuesta{
"project": {
"id": "3a87e7ad-95ba-465d-a820-2bebdd02f0fc",
"slug": "aceros-del-norte-k3m9x",
"externalRef": "CP-2026-0001",
"name": "Nave industrial",
"clientName": "Aceros del Norte",
"status": "draft",
"quoteType": "items",
"paymentDays": 5,
"currency": "MXN",
"totalCents": 14036000,
"portalUrl": "https://miralotu.com/p/aceros-del-norte-k3m9x"
}
}/projects/{id}projects:readDetalle del proyecto
`{id}` es el slug o `ext:{externalRef}`. Incluye etapas, conceptos, hitos (con su monto y si ya se cumplió), pagos, avances, propuestas y accesos del cliente.
Respuesta{
"project": {
"id": "3a87e7ad-95ba-465d-a820-2bebdd02f0fc",
"slug": "aceros-del-norte-k3m9x",
"externalRef": "CP-2026-0001",
"name": "Nave industrial",
"clientName": "Aceros del Norte",
"status": "draft",
"quoteType": "items",
"paymentDays": 5,
"currency": "MXN",
"totalCents": 14036000,
"portalUrl": "https://miralotu.com/p/aceros-del-norte-k3m9x"
},
"stats": {
"progress": 40,
"hoursUsed": 12,
"hoursRemaining": 0,
"paidCents": 7018000,
"totalCents": 14036000
},
"stages": [
{
"position": 1,
"name": "Ingeniería",
"status": "done",
"progress": 100
}
],
"items": [
{
"position": 1,
"description": "Instalación eléctrica tablero principal",
"unit": "lote",
"quantity": 1,
"unitPriceCents": 8500000
},
{
"position": 2,
"description": "Tornillo 1/4",
"unit": "pza",
"quantity": 500,
"unitPriceCents": 12.34
}
],
"milestones": [
{
"position": 1,
"label": "Anticipo",
"trigger": "acceptance",
"condition": "Al aceptar la propuesta",
"amountType": "percent",
"percent": 50,
"amountCents": 7018000,
"reachedAt": "2026-10-02"
}
],
"payments": [
{
"id": "…",
"concept": "Anticipo",
"amountCents": 7018000,
"dueDate": "2026-10-07",
"condition": null,
"automatic": true,
"paidAt": null
}
],
"updates": [],
"proposals": [
{
"folio": "P-2026-0001",
"status": "accepted",
"totalCents": 14036000,
"acceptedName": "Jorge Ramírez"
}
],
"viewers": [],
"pendingInvitations": []
}/projects/{id}projects:writeEditar proyecto
Solo cambia los campos enviados. Si envías `items` o `milestones`, reemplazan los actuales. Para marcarlo entregado: `{ "status": "completed" }` (cumple los hitos «contra entrega»).
Cuerpo{
"status": "active",
"items": [
{
"description": "Tablero principal",
"quantity": 1,
"unitPrice": 90000
}
]
}Respuesta{
"ok": true
}/projects/{id}/stagesprojects:writeReemplazar etapas
Estimación de cada etapa: `estimate` + `unit` (`hours` · `days` · `weeks` · `months`), o `hoursEstimated` (horas). En cotización por horas se convierte a horas con las equivalencias de la empresa (8 h/día, 40 h/semana, 160 h/mes por defecto); por conceptos es solo la duración.
Cuerpo{
"stages": [
{
"name": "Ingeniería",
"estimate": 2,
"unit": "weeks",
"startDate": "2026-10-05",
"endDate": "2026-10-20",
"status": "in_progress",
"progress": 30
}
]
}Respuesta{
"ok": true,
"count": 1
}/projects/{id}/stages/{position}projects:writeActualizar una etapa
Por su número (1, 2, 3…). El avance de las etapas cumple los hitos «al llegar a % de avance».
Cuerpo{
"status": "done"
}Respuesta{
"ok": true,
"stage": {
"position": 1,
"status": "done",
"progress": 100
}
}Propuestas
/projects/{id}/proposalsproposals:writeEnviar propuesta
Genera la propuesta con folio y la manda por correo al contacto del cliente con un link personal. El cliente la acepta (nombre, correo verificado y firma con Face ID o huella) o la rechaza con motivos; te avisamos por webhook. Las anteriores sin responder quedan `superseded`. `cc` (opcional, hasta 10): correos que reciben su propio link solo para ver la propuesta; no la aceptan, rechazan ni descargan. `folio` (opcional): el folio de tu sistema, para que coincida en los dos lados; si no lo mandas, se asigna P-AAAA-NNNN. La propuesta sale con el logo y los datos de la empresa de la llave (Configuración o `PUT /settings`).
Cuerpo{
"to": "compras@aceros.mx",
"cc": [
"direccion@aceros.mx"
],
"message": "Te comparto la propuesta que platicamos.",
"folio": "PRY-2026-0002"
}Respuesta{
"proposal": {
"id": "…",
"folio": "P-2026-0001",
"status": "sent",
"totalCents": 14036000
},
"url": "https://miralotu.com/propuesta/…",
"emailed": true
}/projects/{id}/proposalsprojects:readEstado de las propuestas
`status`: sent · viewed · accepted · rejected · superseded.
Respuesta{
"proposals": [
{
"folio": "P-2026-0001",
"status": "viewed",
"viewedAt": "2026-10-02T21:53:00.000Z"
}
]
}Empresa
/settingsprojects:readDatos de la empresa
Nombre, datos fiscales, contacto y logo con los que salen la propuesta, el portal y los correos.
Respuesta{
"settings": {
"tradeName": "Aceros del Norte",
"legalName": "Aceros del Norte SA de CV",
"taxId": "ANO010101AB1",
"logoUrl": "https://miralotu.com/api/brand/logo?w=…"
}
}/settingssettings:writeSincronizar datos de la empresa
Para que la propuesta, el portal y los correos salgan con los datos de tu sistema (p. ej. cada tenant). Solo se cambian los campos enviados. `logo`: `{ mime: image/png | image/jpeg | image/webp, data: base64 }` (máx. 5 MB; si es el mismo que la vez pasada no se vuelve a procesar) o `null` para quitarlo. No funciona con llaves de la empresa principal.
Cuerpo{
"tradeName": "Aceros del Norte",
"legalName": "Aceros del Norte SA de CV",
"taxId": "ANO010101AB1",
"taxRegime": "601 - General de Ley Personas Morales",
"taxZip": "64000",
"address": "Av. Constitución 100, Monterrey, N.L.",
"phone": "8112345678",
"email": "ventas@aceros.mx",
"logo": {
"mime": "image/png",
"data": "iVBORw0KGgo…"
}
}Respuesta{
"ok": true,
"logoUpdated": true,
"settings": {
"tradeName": "Aceros del Norte",
"logoUrl": "https://miralotu.com/api/brand/logo?w=…"
}
}Avances y solicitudes
/projects/{id}/updatesupdates:writePublicar avance
El cliente lo ve al momento en su portal y recibe un aviso por correo y notificación (sin el detalle). `files`: hasta 10 adjuntos `{ name, mime, base64 }` — imágenes (10 MB, se ven dentro del avance), PDF, Word, Excel, PowerPoint, CSV, ZIP, DWG/DXF o video (25 MB); el tipo se toma de la extensión de `name`. (`images` sigue funcionando.) Con `externalRef` (el id del avance en tu sistema) es idempotente: si ya existe se actualiza y responde 200 con `created: false`; al actualizar no se manda correo ni se crean solicitudes, y las imágenes solo se reemplazan si mandas nuevas.
Cuerpo{
"externalRef": "picoi-update-019f…",
"date": "2026-10-03",
"hours": 6,
"stage": 1,
"stageProgress": 60,
"title": "Levantamiento en sitio",
"summary": "Se midió la nave y se ubicaron los puntos de carga.",
"items": [
{
"text": "Levantamiento de cargas",
"done": true
}
],
"next": [
"Diagrama unifilar"
],
"requests": [
{
"question": "¿Aprueban la ubicación del tablero?",
"type": "approval"
}
]
}Respuesta{
"ok": true,
"id": "…",
"created": true
}/projects/{id}/updates/{updateId}updates:writeBorrar avance
`{updateId}`: id del avance o `ext:{externalRef}`. Las solicitudes y lo que contestó el cliente se conservan.
Respuesta{
"ok": true
}/projects/{id}/requestsprojects:readSolicitudes y respuestas
Respuesta{
"requests": [
{
"id": "…",
"question": "¿Aprueban la ubicación del tablero?",
"type": "approval",
"status": "answered"
}
]
}/projects/{id}/requestsrequests:writeCrear solicitud
`type`: choice · multiple · text · file · approval. `update` (id o `ext:{externalRef}` de un avance) la liga a ese avance. Con `externalRef` es idempotente: si ya existe y el cliente no ha contestado, se actualiza (200). Al crearse se avisa al cliente por correo y notificación (`notify: false` para no avisar). La respuesta del cliente llega por el webhook `request.answered` con tu `externalRef`.
Cuerpo{
"externalRef": "picoi-req-0042",
"update": "ext:picoi-update-019f…",
"question": "Envíanos el plano arquitectónico",
"type": "file",
"description": "PDF o DWG"
}Respuesta{
"ok": true,
"id": "…",
"created": true
}/projects/{id}/requests/{requestId}requests:writeCerrar o reabrir solicitud
`{requestId}`: id o `ext:{externalRef}`. Cerrada, el cliente ya no puede cambiar su respuesta.
Cuerpo{
"status": "closed"
}Respuesta{
"ok": true
}Pagos y accesos
/projects/{id}/paymentspayments:writeAgregar pago al calendario
Pagos aparte de los automáticos (por periodo o por hito). Monto en pesos. Con `externalRef` es idempotente (si ya existe y no está pagado, se actualiza).
Cuerpo{
"externalRef": "picoi-cxc-0012",
"concept": "Extra: tablero adicional",
"amount": 18500,
"dueDate": "2026-11-15"
}Respuesta{
"ok": true,
"id": "…"
}/projects/{id}/payments/{paymentId}/recordpayments:writeRegistrar pago recibido
`{paymentId}`: id del pago, `ext:{externalRef}`, `milestone:{n}` (pago del hito n) o `period:{n}` (pago del mes o etapa n). `amount` en pesos solo si difiere del monto. Volver a mandarlo actualiza el registro. El cliente ve el pago como «Pagado» y se dispara el webhook `payment.recorded`. Para deshacerlo: `DELETE` a la misma ruta.
Cuerpo{
"paidAt": "2026-10-06",
"amount": 107276.8,
"reference": "SPEI 0912"
}Respuesta{
"ok": true,
"id": "…",
"paidAt": "2026-10-06",
"paidAmountCents": 10727680
}/projects/{id}/invitationsinvitations:writeInvitar al portal
Manda el link personal (de un solo uso) para que la persona active su acceso con Face ID o huella.
Cuerpo{
"name": "Ana López",
"email": "ana@aceros.mx",
"role": "Administración"
}Respuesta{
"ok": true,
"url": "https://miralotu.com/p/…?t=…",
"emailed": true
}Plataforma
/workspacesllave del servidorDar de alta una empresa
Con la llave del servidor, o con la de una cuenta con **plan Integrador** (permiso `workspaces:write`). Crea la empresa, invita a su responsable por correo y devuelve una llave de API para esa empresa (solo se muestra aquí). Un integrador da de alta así a cada negocio que conecta: cada empresa integrada tiene su marca, no tiene límite de API y se cobra al integrador por mes desde su alta. `freeMonths` (opcional): meses gratis de promoción.
Cuerpo{
"name": "Opsiris",
"ownerName": "Yamili",
"ownerEmail": "admin@opsiris.mx",
"externalRef": "picoi-tenant-123",
"freeMonths": 2
}Respuesta{
"workspace": {
"id": "…",
"name": "Opsiris",
"externalRef": "picoi-tenant-123"
},
"owner": {
"email": "admin@opsiris.mx",
"emailed": true
},
"apiKey": "mlt_…"
}/workspacesllave del servidorListar empresas
Con la llave del servidor, todas; con la de un integrador, solo las empresas que integra.
Respuesta{
"workspaces": [
{
"id": "…",
"name": "Opsiris",
"externalRef": "picoi-tenant-123",
"disabled": false,
"projects": 3
}
]
}Webhooks
Registra tu URL en Integraciones y elige los eventos. Te avisamos con un POST en JSON; responde con cualquier código 2xx. Si falla, reintentamos a los 1, 5 y 30 minutos, 2 y 12 horas (6 intentos). El encabezado Miralotu-Delivery es único por envío: úsalo para no procesar dos veces el mismo aviso.
| proposal.sent | Se envió una propuesta al cliente |
| proposal.viewed | El cliente abrió la propuesta |
| proposal.accepted | El cliente aceptó y firmó la propuesta |
| proposal.rejected | El cliente rechazó la propuesta (con motivos) |
| project.authorized | El proyecto quedó autorizado |
| update.published | Se publicó un avance |
| request.answered | El cliente contestó una solicitud |
| payment.recorded | Se registró un pago recibido |
| viewer.registered | Una persona del cliente activó su acceso |
POST https://tu-sistema.com/webhooks/miralotu
Miralotu-Event: proposal.accepted
Miralotu-Delivery: 5b1f…
Miralotu-Signature: t=1790980000,v1=3c9a…
{
"id": "5b1f…",
"event": "proposal.accepted",
"createdAt": "2026-10-02T22:00:00.000Z",
"workspaceId": "…",
"data": {
"project": {
"id": "…",
"slug": "aceros-del-norte-k3m9x",
"externalRef": "CP-2026-0001",
"name": "Nave industrial",
"status": "active",
"totalCents": 14036000
},
"viewer": {
"id": "…",
"name": "Jorge Ramírez",
"email": "compras@aceros.mx"
},
"proposal": {
"folio": "P-2026-0001",
"status": "accepted",
"acceptedName": "Jorge Ramírez",
"acceptedDevice": "Android · Chrome · Huella",
"contentHash": "9f2c…"
}
}
}Verifica la firma con el secreto del webhook (lo ves en Integraciones): es un HMAC-SHA256 de {t}.{cuerpo}.
import crypto from "node:crypto";
// En tu ruta del webhook, con el cuerpo SIN procesar (texto tal como llegó):
export function verifyMiralotu(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // más de 5 min: se rechaza
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}Flujo de una cotización
POST /projectsconquoteType: "items", tus conceptos, hitos de pago yexternalRef= tu folio.POST /projects/ext:{folio}/proposals: el cliente recibe el correo con su propuesta.- Recibes
proposal.viewedy luegoproposal.accepted(con nombre, dispositivo y huella SHA-256 del contenido) oproposal.rejected(con motivos). - Al aceptar, el proyecto queda autorizado y el calendario de pagos se genera solo con tus hitos. El cliente ve todo en su portal.
- Publica avances con
POST /projects/{id}/updatesy actualiza etapas: el % de avance cumple los hitos de pago. Al terminar,PATCH { "status": "completed" }.