Guía de integración con ERP y otros sistemas
Delimite una integración antes de construirla: qué alcanza la API, qué no alcanza y dónde están los límites
Esta guía está dirigida al desarrollador que delimita una integración entre FreeMaint y otro sistema: un ERP, un historiador SCADA, un software de almacén o de contabilidad. Los demás artículos de esta sección explican cómo llamar a la API; este habla deliberadamente de fronteras, porque el error caro nunca es una cabecera equivocada: es descubrir a mitad del desarrollo que una capacidad que daba por hecha no existe. Cada límite que aparece más abajo está enunciado a propósito.
Una clave, tres puertas
Un administrador crea la clave de API (empieza por fmk_) en Configuración de la empresa > API (/company-settings/api), o bien una persona la crea para sí misma en Perfil > Seguridad > Claves de API, y se envía como Authorization: Bearer fmk_... . Autentica exactamente tres cosas: el endpoint GraphQL en POST /api/v1/graphql, el endpoint de ingesta de energía, y /public-api/whoami y /public-api/ping. NO autentica las rutas REST bajo /api/v1/{resource}: esas son la API interna de la aplicación y exigen una sesión de usuario (JWT). Construya su integración sobre GraphQL: esa es la superficie que alcanza una clave.
Lo que le da una clave de API
La clave en claro se muestra una sola vez, al crearla, y después ya no se puede recuperar: solo se guarda un hash SHA-256, junto con los 6 caracteres que siguen a fmk_ para que la lista pueda identificar la clave en pantalla.
Nombre
Una etiqueta legible: «Pasarela ERP Oracle», «Terminal de la línea 2». Emita una clave por cada sistema consumidor, así podrá revocar una sin molestar a las demás.
Nivel de acceso
READ, READ_STOCK o FULL, una jerarquía ordenada por encima de los permisos del propietario. READ solo ejecuta consultas y es el valor predeterminado. READ_STOCK añade la mutación adjustStock. FULL autoriza todas las operaciones que permite el rol del propietario, incluidas la creación, la modificación y la eliminación — y el endpoint de ingesta de energía cuando el propietario puede crear lecturas de energía.
Caducidad
Un expiresAt opcional. Pasado ese momento, la clave deja de autenticar. Déjelo vacío para una integración de servicio permanente; rellénelo para un contratista o un piloto.
Revocación
isActive es un interruptor por clave que surte efecto en segundos. La fila se conserva para la auditoría, de modo que una clave revocada sigue siendo rastreable en lugar de desaparecer.
Último uso
lastUsedAt registra la última llamada autenticada: suficiente para detectar una clave que ya no usa nadie.
Propietario
La persona en cuyo nombre actúa la clave. Cada llamada lleva sus permisos y su visibilidad de datos, y su nombre firma lo que escribe la clave. Para un ERP, cree un usuario dedicado —«Integración ERP»— con un rol que cubra exactamente lo que necesita la pasarela, y asígnele la clave.
Lo que NO le da una clave de API
Son límites reales de la implementación actual, no olvidos de esta página. Si su revisión de seguridad depende de alguno de ellos, diseñe teniéndolos en cuenta ahora y no después de construir.
- Sin lista de módulos por clave. Una clave lee y escribe lo que permite el rol de su propietario: para restringirla, asígnela a un propietario con un rol más restringido. Por encima de eso solo se aplican los tres niveles de acceso: no puede enumerar, en la propia clave, los módulos que puede usar.
- Sin lista blanca de direcciones IP. Una clave es válida desde cualquier punto de Internet. Trátela como un secreto: una bóveda o el gestor de secretos de su plataforma, nunca un repositorio, una unidad compartida o un ticket de soporte.
- Sin registro de uso por clave. Solo se conserva lastUsedAt. No hay historial de llamadas por clave, ni cuota por clave, ni constancia de qué clave realizó qué operación. El contador de cuota diaria es por EMPRESA y lo comparten todas las claves, así que una integración desbocada puede agotar la cuota de todas las demás.
- Sin aprobaciones en nombre de otra persona. Una clave nunca aprueba un paso del flujo de trabajo, aunque su propietario sí pudiera hacerlo: las aprobaciones siguen siendo un acto humano en la aplicación.
- Una empresa por clave. Una clave queda ligada a la empresa en la que se creó y no puede acceder a los datos de otra empresa: un registro de otra empresa simplemente no se encuentra.
Lo que puede leer
GraphQL expone 16 consultas de lectura: whoami, workOrders, workOrder(id), assets, asset(id), assetDowntimes, meters(assetId), parts, part (por id o código de barras), stockMovements, locations, vendors, customers, purchaseOrders, purchaseOrder(id | reference | erpReference) y floorPlans(locationId). Todas las listas se pueden recorrer hasta el final: workOrders, assets, assetDowntimes, meters, parts, purchaseOrders, locations, vendors y customers aceptan limit (de 1 a 200) más offset, y workOrders, assets, parts, purchaseOrders, locations, vendors y customers aceptan además updatedSince para sincronizaciones incrementales. La página por defecto es de 50 filas, salvo en locations, vendors y customers, que conservan su antiguo valor por defecto de 200. Cada lista se ordena con un criterio de desempate sobre id, de modo que un límite de página nunca omite ni repite una fila. stockMovements es el libro de movimientos de inventario con el que se concilia: se lee del más antiguo al más reciente y se pagina con afterId —pase el id del último movimiento recibido—, lo que sigue siendo exacto mientras se escriben nuevos movimientos, y since permite iniciar una carga del histórico a partir de una fecha. Las órdenes de trabajo que están en la papelera nunca se devuelven. purchaseOrders también se filtra por status y vendorId; los importes están en la moneda de la empresa, que cada orden lleva en su campo currency.
Verificar la clave y leer órdenes de trabajo en una sola llamada: curl -X POST https://freemaint.com/api/v1/graphql -H "Authorization: Bearer $FMK" -H "Content-Type: application/json" -d '{"query":"{ whoami { companyId tier } workOrders(limit: 5, status: \"OPEN\") { id reference title status priority dueDate } }"}'
Lo que puede escribir
Doce mutaciones: crear, actualizar y borrar sobre órdenes de trabajo, activos y repuestos, más adjustStock para un movimiento de inventario con signo. Se ejecutan a través de los mismos servicios que usa la propia aplicación, así que las referencias (WO-…, ASS-…, PART-…), las filas de auditoría y las notificaciones se comportan exactamente igual que en la interfaz. Para el stock, prefiera adjustStock: escribe una fila de auditoría StockMovement en la misma transacción y se niega a dejar una quantity por debajo de cero, mientras que updatePart(quantity) fija un valor absoluto y registra la diferencia como un ajuste. En un repuesto controlado por ubicación, pase locationId a adjustStock; allí updatePart(quantity) se rechaza. La undécima, addMeterReading, registra una lectura de contador a través del mismo servicio que la pantalla de contadores, así que los disparadores y el mantenimiento preventivo por contador reaccionan exactamente igual que con una lectura introducida en la aplicación. La duodécima, setPurchaseOrderErpReference, guarda el número de documento de su ERP en una orden de compra para poder emparejar después ambos registros con purchaseOrder(erpReference: …). Requiere una clave de acceso FULL y, a propósito, no dispara ningún webhook: su ERP nunca recibe su propia escritura como evento.
Lo que la API no cubre en absoluto
El esquema GraphQL contiene órdenes de trabajo, activos, paradas de máquina, repuestos y sus movimientos de inventario, contadores, órdenes de compra, ubicaciones, proveedores, clientes y planos. Nada más. Lo siguiente son partes plenamente funcionales de FreeMaint que hoy NO tienen superficie de API: no se pueden leer, escribir ni suscribir desde una integración, y ninguna combinación de las consultas existentes las alcanza.
- Las inspecciones: ni las plantillas, ni las ejecuciones programadas, ni los resultados registrados, ni los códigos de acción. No hay ninguna consulta de inspección, ninguna mutación de inspección y ningún evento de webhook de inspección.
- Usuarios, roles y equipos: no puede listar, crear ni desactivar un usuario a través de esta API. El aprovisionamiento de usuarios se hace en la aplicación; en el plan Enterprise puede automatizarse con SCIM, que es un mecanismo aparte y no forma parte de esta API.
- Proyectos: ninguna consulta y ninguna mutación.
- Solicitudes de compra: ninguna consulta y ninguna mutación. Las órdenes de compra sí se pueden leer (purchaseOrders, purchaseOrder), su ERP puede escribir en ellas su referencia (setPurchaseOrderErpReference) y los webhooks purchase_order.* las anuncian, pero una orden no se puede crear, modificar ni recibir a través de la API: eso sigue haciéndose en la aplicación.
- Archivos, fotos y adjuntos: no puede subir un documento ni una foto, y tampoco puede volver a leerlos. Las fotos de las órdenes de trabajo, las imágenes de los repuestos y las evidencias de inspección quedan todas fuera de la API. Una URL que aparezca en una respuesta, como el imageUrl de un repuesto, no es una ruta de subida.
- Los planes de mantenimiento preventivo, los partes de horas, las tareas y las listas de comprobación: no están expuestos.
Importante
Webhooks: el catálogo de eventos
Los webhooks son la mitad «push» de una integración: en lugar de que usted consulte, es FreeMaint quien hace un POST a su URL en el momento en que ocurre algo. Se configuran en Configuración de la empresa > Webhooks (/company-settings/webhooks) y cada webhook se suscribe a los eventos que necesite. El catálogo es una lista fija.
- work_order.created — se creó una orden de trabajo
- work_order.completed — una orden de trabajo pasó a Completada
- work_order.status_changed — una orden de trabajo cambió a cualquier estado; la carga útil lleva new_status. Se dispara además de work_order.completed, que no cambia
- request.created — se envió una solicitud de mantenimiento
- request.status_changed — una solicitud fue aprobada, rechazada o cancelada
- part.low_stock — un repuesto bajó hasta su mínimo o por debajo
- part.out_of_stock — un repuesto llegó a cero; es un evento aparte, no una variante de low_stock
- part.low_stock_summary — un resumen diario por empresa con los repuestos en su punto de pedido o por debajo, incluidos los que nunca se movieron
- part.out_of_stock_summary — el mismo resumen diario, para los repuestos que están a cero
- part.consumed — un repuesto se consumió en una orden de trabajo, o se devolvió de ella; quantity lleva signo
- compliance.chain_divergence — se rompió la cadena de hash de auditoría (Enterprise)
- asset.downtime_started — se abrió una parada de máquina
- asset.downtime_ended — se cerró una parada de máquina; ya se puede imputar la duración
- report.daily_summary — los trabajos completados del día, una vez al día a la hora que elija
- purchase_order.created — se creó una orden de compra; la carga útil incluye la orden (reference, status, total_cost, currency, expected_date, erp_reference) y su proveedor
- purchase_order.status_changed — una orden de compra cambió de estado (aprobada, pedida, cancelada…); la carga útil incluye previous_status y new_status
- purchase_order.received — se recibió una entrega de una orden de compra (se envía en cada entrega, incluidas las parciales); la carga útil lista cada línea con quantity_ordered, quantity_received (esta entrega), quantity_received_total y quantity_remaining, además de receipt_id, fully_received y closed_short; new_status es PARTIALLY_RECEIVED mientras se espera mercancía y RECEIVED cuando el pedido está completo o cerrado
Advertencia
El sobre de la entrega
Cada entrega GENERIC es un POST cuyo cuerpo es el mismo sobre de tres claves: event, timestamp y data, donde data está indexado por entidad. Le acompañan tres cabeceras. X-FreeMaint-Signature tiene la forma t=<segundos unix>,v1=<hex>, donde v1 es el HMAC-SHA256 de la cadena exacta «<t>.<cuerpo en bruto>» firmada con el secreto de ese webhook; X-FreeMaint-Event repite el nombre del evento; X-FreeMaint-Delivery-Id identifica la entrega. Verifique la firma sobre el cuerpo EN BRUTO, antes de parsearlo: volver a serializar el JSON cambia los bytes y la firma ya no coincidirá.
{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "Fuga en la bomba n.º 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }
Fiabilidad y supervisión
Una entrega se intenta 3 veces con retroceso exponencial sobre una base de 2 segundos — aproximadamente 2 s, luego 4 s, luego 8 s — y cada intento expira a los 10 segundos. Un 2xx la marca como entregada. Las respuestas 5xx y los errores de red se reintentan. Un 4xx se trata como un rechazo definitivo y NO se reintenta en absoluto: si su endpoint responde 400 o 401 por un fallo de su lado, esa entrega se detiene ahí y solo un reenvío manual la recuperará.
- Cada entrega se almacena: el evento, la carga útil exacta enviada, el estado, el código HTTP que devolvió, los primeros ~500 caracteres del cuerpo de su respuesta, el número de intentos y el mensaje de error.
- Los estados son PENDING, DELIVERED, FAILED, RATE_LIMITED (se alcanzó el tope de 100 entregas al día de Starter) y SKIPPED_TIER (el plan ya no da derecho a ese destino).
- Una entrega almacenada se puede reenviar desde el historial del webhook. Reenvía la carga útil guardada sin cambios, así que las claves de negocio que contiene siguen siendo idénticas y un receptor que deduplique correctamente no la registrará dos veces.
- Cada webhook lleva lastTriggeredAt, lastSuccessAt, lastFailureAt y un failureCount, suficiente para una comprobación de salud sin abrir el historial completo.
- La lista de webhooks y su registro de entregas se pueden consultar en todos los planes, así que una integración detenida por una bajada de plan queda a la vista en lugar de morir en silencio.
- Nada le avisa cuando las entregas empiezan a fallar: ni correo, ni notificación. Si una caída silenciosa fuese un problema, consulte el registro de entregas desde su propia supervisión.
Seguridad
Las URL se validan al guardar el webhook, y las reglas son estrictas a propósito: un webhook es una petición saliente que hacen nuestros servidores, así que una regla permisiva sería un vector de SSRF.
- Las claves se almacenan como hashes SHA-256: el valor en claro no se puede recuperar de FreeMaint después de crearlo.
- Las URL de webhook deben usar https. El http simple y cualquier otro esquema se rechazan.
- Solo se aceptan los puertos 443 y 8443.
- Se rechazan los hosts de bucle local, de enlace local y de rangos privados (127.x, 10.x, 192.168.x, 172.16–31.x, ::1 y similares): el receptor debe ser accesible desde la Internet pública. Un sistema que solo existe dentro de su planta necesita un relé público o un túnel.
- Las credenciales en la URL (https://user:pass@host) se rechazan.
- No se siguen las redirecciones: el endpoint que registra es el endpoint que recibe el POST.
- Cada webhook tiene su propio secreto de firma y existe una acción de rotación del secreto. La rotación surte efecto de inmediato, así que despliegue el nuevo secreto de su lado en la misma ventana.
- Los destinos SLACK y TEAMS reciben un mensaje con el formato de la plataforma y NINGUNA cabecera HMAC de FreeMaint: para ellos, el secreto de la URL es la única autenticación. Use GENERIC para todo lo que necesite verificar criptográficamente.
Patrones que funcionan bien hoy
Estas son las integraciones que la superficie actual soporta con limpieza, y el detalle que hace que cada una sea segura en producción.
Reflejar el consumo en un ERP
Suscríbase a part.consumed. quantity lleva signo — negativo cuando el repuesto sale hacia el trabajo, positivo cuando se devuelve — y la carga útil incluye un stock_movement_id que se mantiene estable entre reintentos y reenvíos, así que úselo como clave de idempotencia. La mutación adjustStock deliberadamente NO lanza este evento, y eso es lo que evita que sus propias escrituras le vuelvan en eco formando un bucle.
Enviar correcciones de stock
Llame a adjustStock con un delta con signo desde su lado. Una clave READ_STOCK puede hacerlo sin poder crear, modificar ni eliminar nada, lo que la convierte en una opción ajustada para un puente de stock unidireccional.
Crear órdenes de trabajo desde otro sistema
createWorkOrder a partir de una alarma SCADA, un ticket de soporte o un aviso de avería del ERP. La orden de trabajo recibe su referencia normal y lanza work_order.created como cualquier otra.
Mantener sincronizado un catálogo de repuestos
parts(updatedSince:) devuelve solo lo que ha cambiado desde el momento que usted elija. createPart y updatePart aceptan customReference — su propio código para el repuesto — y part(barcode:) busca luego por él, de modo que puede localizar un repuesto por el identificador que su ERP ya utiliza. No se impone que sea único; eso le corresponde garantizarlo a usted.
Cerrar el ciclo al completar
work_order.completed transporta la orden de trabajo y su estado anterior, lo que basta para cerrar un aviso de mantenimiento en el otro lado.
Qué plan necesita tener
Dos condiciones independientes: una para la API y otra para los webhooks. Una empresa en Starter puede recibir webhooks sin tener la API.
API GraphQL y claves de API
Business y superiores. Core y Starter no pueden crear ninguna clave.
Cuota diaria
5.000 llamadas al día en Business, 100.000 en Enterprise. Se cuentan por empresa y por operación, y se reinician a las 00:00 UTC — agrupar operaciones en una sola petición HTTP ahorra idas y vueltas, nunca cuota.
Webhooks
Starter y superiores, con un tope de 100 entregas al día; ilimitadas desde Business.
Destinos Slack y Teams
Enterprise. Un webhook genérico está disponible desde Starter.
Consejo
Artículos relacionados
¿Te resultó útil esta página?