Apariencia
Integrar un ERP
Esta guía es para quien programa la integración entre el ERP de la empresa y Safety Panel. Recorre el circuito completo en el orden en que conviene hacerlo. Cada paso enlaza a Recursos, donde están los campos, los filtros y los errores de cada llamada, y a Convenciones, que vale para todas. Si generás un cliente, partí de la especificación OpenAPI.
Quién es dueño de cada dato
El ERP ya lleva el personal, los contratos, el catálogo y el stock. Lo que Safety Panel aporta es la evidencia firmada. Cada dato tiene un solo dueño, y el otro sistema lo copia:
| Dato | Dueño | Sentido | Recurso |
|---|---|---|---|
| Empleados, legajo, empleador, sitio y puesto asignados | ERP o RRHH | ERP → Safety Panel | Empleados |
| Sitios: alta y baja de contratos | ERP | ERP → Safety Panel | Sitios |
| Catálogo de EPP: código, marca, modelo | ERP | ERP → Safety Panel | Productos de EPP |
| Stock de EPP | ERP | ERP → Safety Panel, o apagado en Safety Panel | Stock de EPP |
| Entrega firmada, devoluciones y constancia | Safety Panel | Safety Panel → ERP | Entregas de EPP |
| Capacitaciones, constancias e incidentes | Safety Panel | Safety Panel → ERP | Constancias, registros e incidentes |
Dos reglas valen para todo el circuito:
- Nada se borra por la API. Los empleados se dan de baja y los sitios se archivan, y los dos se restauran con todo su historial. La evidencia se conserva siempre.
- La firma ocurre siempre en Safety Panel. El ERP registra lo que despachó, pero solo el empleado firma, en su portal o en el tótem.
1. Emití una key para la integración
Emití una key solo para el ERP, con los permisos que va a usar y ninguno más. Si otro sistema (por ejemplo, el de RRHH) también se conecta, que tenga su propia key: así cada uno se revoca sin afectar al otro.
| Para | Permisos |
|---|---|
| El personal | employees:write, sites:write, legal_entities:read y positions:read. Con legal_entities:write y positions:write, además, guardás tus códigos en las razones sociales y los puestos. |
| El catálogo y el stock | ppe_products:write, ppe_categories:read y ppe_stocks:write. |
| Las entregas | ppe_deliveries:write para crearlas y leerlas, ppe_needs:read para planificar y ppe_historical_deliveries:write para cargar el historial. |
| La evidencia | certifications:read y training_records:read. incidents:read solo si el ERP cruza los accidentes con el ausentismo. |
Ningún permiso se da solo, ni incidents:read ni ningún otro (ver Permisos).
Si el ERP ya usaba una key anterior a los permisos, esa key solo lee la cuenta, los sitios, los grupos de sitios y los profesionales, y en ellos solo modifica sus campos externos: no da de alta, no archiva ni llega a los demás recursos de esta guía. Emití una nueva con los permisos de la tabla, cambiala en el ERP y revocá la anterior.
2. La carga inicial
No hay endpoints de carga masiva: la carga inicial es una llamada por registro. Hacela en este orden, porque cada paso usa los id del anterior:
- Razones sociales. Leé las razones sociales de la cuenta y asociá cada empresa del ERP con la suya (buscala con
tax_id). No se crean por la API: un empleador nuevo se agrega en el panel. - Sitios. Uno por contrato, con
POST /api/v1/sites. Si el sitio ya existe en Safety Panel, guardale tu código conPATCHen lugar de crearlo. Cargale la dirección: las entregas de EPP la toman del sitio. Un sitio creado por la API nace con los módulos apagados: para registrar entregas de EPP en él, encendé Entregas de EPP en ese sitio desde el panel (ver Habilitación). - Puestos. Leé los puestos de cada sitio y asocialos con los del ERP. Tampoco se crean por la API: se cargan en el panel.
- Empleados. Con
POST /api/v1/employees, cada uno con sulegal_entity_id. - Asignaciones. Cada sitio y puesto de cada empleado, con
PUT /api/v1/employees/:id/sites/:site_id. - Catálogo. Cada producto con
POST /api/v1/ppe_productsy su código enexternal_reference, y habilitado en sus sitios conPUT /api/v1/ppe_products/:id/sites/:site_id. - Stock, si Safety Panel lo refleja:
PUT /api/v1/sites/:site_id/ppe_stocks/:ppe_product_idpor producto y sitio. Ver El stock. - Historial de entregas. Lo que el ERP ya entregó, con
POST /api/v1/employees/:employee_id/ppe_historical_deliveries. Así las necesidades parten de la última entrega real. Esas entregas no son evidencia de firma: no tienen constancia ni descuentan stock. Ver Cargar el historial de entregas.
El ritmo
Hacé una pausa de 200 ms entre llamadas: son las 300 por minuto del límite de uso de la key. Cuentan todas las llamadas de la key, también las lecturas con que el ERP busca un registro, así que si la misma key hace otras llamadas mientras tanto, la pausa tiene que ser más larga. Ante un 429, esperá los segundos de retry-after y reintentá el mismo registro, no el siguiente.
Que un reintento no cree dos veces
Cada alta lleva el identificador del registro en el ERP en dos lugares:
- en
external_reference, tal cual, para encontrarlo después; - en el encabezado
Idempotency-Key, precedido por el recurso:employee-E-123,site-C-981,ppe_product-GUA-NIT-09.
El prefijo hace falta porque las claves valen para toda la key, no por recurso: si un sitio y un empleado comparten el número 1001 en el ERP, la segunda alta respondería 422 idempotency_key_reused en lugar de crearse.
Con esa clave, un reintento dentro de las 24 horas no crea el registro dos veces. Pasado ese plazo, buscalo por su external_reference antes de volver a crearlo: la búsqueda trae también los dados de baja y los archivados. Para las demás escrituras sobre un registro (una baja, una asignación), usá una clave nueva por operación.
Un ejemplo
Este bucle en Ruby crea un registro por llamada con la pausa, y reintenta con la misma clave y el mismo cuerpo un 429, un 409 idempotency_key_in_progress, un corte de red y un error de la puerta de entrada (502, 503, 504), que pueden llegar aunque el registro se haya guardado. Si un registro no responde después de 10 intentos, se detiene:
ruby
require "net/http"
require "json"
PAUSE = 0.2
ATTEMPTS = 10
RETRIABLE = %w[502 503 504].freeze
def create(http, token, path, key, record)
ATTEMPTS.times do
request = Net::HTTP::Post.new(path, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json",
"Idempotency-Key" => key)
request.body = JSON.generate(record)
begin
response = http.request(request)
rescue Net::OpenTimeout, Net::ReadTimeout, EOFError, SystemCallError
sleep 5
next
end
if response.code == "429"
sleep Integer(response["retry-after"])
elsif RETRIABLE.include?(response.code) || in_progress?(response)
sleep 5
else
sleep PAUSE
return response
end
end
raise "#{key}: sin respuesta después de #{ATTEMPTS} intentos"
end
def in_progress?(response)
response.code == "409" && JSON.parse(response.body).dig("error", "code") == "idempotency_key_in_progress"
rescue JSON::ParserError
false
endSe usa así, con un registro del ERP por llamada:
ruby
Net::HTTP.start("tuempresa.safetypanel.com.ar", 443, use_ssl: true) do |http|
employees.each do |employee|
key = "employee-#{employee["external_reference"]}"
response = create(http, ENV.fetch("SAFETYPANEL_API_KEY"), "/api/v1/employees", key, employee)
puts "#{employee["external_reference"]}: #{response.code}"
end
endCualquier otra respuesta vuelve a tu sistema: un 201 trae el registro creado, y un 4xx dice en el mensaje qué corregir. Si la carga se detiene, volvé a correrla: dentro de las 24 horas, lo que ya se creó responde lo mismo sin duplicarse.
Una excepción: si al retomar la carga un registro cambió en el ERP, su alta ya no es el mismo pedido y responde 422 idempotency_key_reused. Ese registro sí se creó la primera vez. No lo vuelvas a crear con otra clave: buscalo por su external_reference y actualizalo con PATCH. Cualquier otro 4xx no guardó nada.
3. El personal
Altas
Un empleado nuevo en el ERP es un POST /api/v1/employees, y después un PUT por cada sitio donde trabaja. Ver Dar de alta un empleado y Asignar a un sitio.
Bajas
Cuando alguien deja la empresa, dalo de baja con PATCH /api/v1/employees/:id y {"status": "deleted"}, con una Idempotency-Key nueva: todo PATCH que envía status o legal_entity_id la exige, aunque sea el valor que ya tiene, igual que al reactivar. No hay DELETE: se conserva con todo su historial, sus entregas firmadas y sus constancias. Ver Dar de baja y reactivar.
Recontrataciones
Recontratar es reactivar, nunca crear de nuevo. Antes del alta, buscá a la persona por su DNI con GET /api/v1/employees?dni=…, que la encuentra en cualquier estado. Si está dada de baja, reactivala con PATCH y {"status": "active"}.
Si el alta llega igual, Safety Panel la frena: responde 422 employee_deactivated con el employee_id del empleado existente. Reactivá ese en lugar de crear otro.
Al reactivarlo conserva los sitios que tenía. Ajustalos con PUT y DELETE sobre sus sitios.
Cambios de sitio y de puesto
Un empleado que rota de sitio se mueve de a un sitio por llamada:
PUT /api/v1/employees/:id/sites/:site_idlo asigna, o le cambia el puesto si ya estaba.DELETE /api/v1/employees/:id/sites/:site_idlo quita del sitio, sin borrar nada de lo que firmó ahí.
Las dos se pueden repetir sin efecto, así que el ERP puede enviar la asignación que tiene registrada sin preguntar antes. Ver Asignar a un sitio y Quitar de un sitio.
Contratos que terminan y vuelven
Cuando termina un contrato, archivá su sitio con PATCH /api/v1/sites/:id y {"status": "deleted"}, con una Idempotency-Key nueva; restaurarlo también la exige. Si el contrato vuelve, restauralo con {"status": "active"} en lugar de crear otro: lo encontrás por su external_reference. Ver Archivar y restaurar.
4. El circuito de EPP
El catálogo
Cada producto lleva en external_reference su código (SKU) en el ERP. Safety Panel no exige que el código sea único: si dos productos lo comparten, una entrega que lo nombra responde 422 ambiguous_external_reference con los matching_ids, en lugar de elegir uno en silencio. Corregí el código en el ERP o nombrá el producto por su ppe_product_id. Ver Productos de EPP.
La entrega
- El ERP registra lo que despachó con
POST /api/v1/ppe_deliveries: el empleado, el sitio, los elementos con su código y el número de remito enexternal_reference. Puede quedar en borrador (draft), para completarla después, o lista para firmar (ready_to_sign). Ver Crear una entrega. - El empleado la firma en Safety Panel, en su portal o en el tótem.
- El ERP se entera de la firma de una de dos formas:
- con un webhook del evento
ppe_delivery.signed, que sale al firmarse y se reintenta si tu sistema no responde. Ver Recibir eventos con un webhook; - o consultando cada tanto con
GET /api/v1/ppe_deliveries?status=signed&signed_since=….
- con un webhook del evento
- Recién ahí el ERP descuenta su stock. Una entrega
drafttodavía puede cambiar, y unadraftoready_to_signpuede cancelarse: no la tomes como entregada. - El ERP guarda la constancia. La constancia en PDF se genera unos segundos después de la firma, así que puede no estar todavía al llegar el aviso. Ver La constancia en PDF.
Una devolución avisa con ppe_delivery_return.created: es el momento de que el ERP sume al stock lo devuelto. Una entrega que no se va a firmar se cancela con PATCH, su motivo y una Idempotency-Key nueva. Ver Modificar o cancelar una entrega.
El stock
El dueño del stock es el ERP. Hay dos caminos:
- Safety Panel lo refleja. El ERP empuja la cantidad absoluta de cada producto en cada sitio con
PUT, y el sitio ve cuánto tiene y recibe el aviso de stock bajo. Safety Panel sigue descontando al firmarse una entrega, pero el últimoPUTdel ERP es el que vale: el número de Safety Panel se pone al día con él. Para que coincida enseguida, enviá elPUTen cuanto el ERP procese cada firma o devolución. Ver Quién descuenta. - Safety Panel no lo lleva. Si no querés ese reflejo, pedí a Safety Panel que apague la gestión de stock en la cuenta. Las entregas funcionan igual, y un
PUTde stock responde409ppe_stock_disabled.
Las necesidades
Para planificar compras y despachos, leé qué le falta a cada empleado según su puesto con GET /api/v1/ppe_needs?site_id=…&status=missing. Se calculan en el momento con la misma regla que el panel: no las copies, pedilas cada vez. Ver Necesidades de EPP.
5. La evidencia que lee el ERP
Lo que el ERP no tiene, y lee de Safety Panel para su legajo:
| Qué | Cómo | Para enterarse |
|---|---|---|
| Las entregas firmadas y su constancia | Entregas de EPP. El enlace a la constancia vence a los 5 minutos: descargala en el momento. | ppe_delivery.signed y ppe_delivery_return.created |
| Las constancias de capacitación y su certificado | Constancias de capacitación. El certificado se descarga con la key, a no más de 30 por minuto, y cada descarga cuenta también en las 300. | certification.expiring y certification.expired |
| Las capacitaciones presenciales y quiénes asistieron | Registros de capacitación. | updated_since |
| Los incidentes | Incidentes, con incidents:read. Sin datos de salud ni relatos. | incident.created |
Las constancias y los certificados son evidencia firmada con datos personales: guardalos como documentos confidenciales.
Para todo lo demás, y para lo que se haya perdido mientras un webhook estuvo desactivado, leé lo que cambió con updated_since. Ver Sincronizar con otro sistema.
Si una llamada falla
Ver qué hacer con cada respuesta en Sincronizar con otro sistema, y todos los códigos en Errores.