Apariencia
Convenciones
Lo que se describe en esta página vale para todos los recursos.
Dirección
La API responde en el subdominio de la cuenta, bajo /api/v1/:
https://<subdominio>.safetypanel.com.ar/api/v1/El subdominio es la parte inicial de la dirección con la que se ingresa al panel (por ejemplo, para tuempresa.safetypanel.com.ar es tuempresa). La dirección exacta de la cuenta aparece en Configuración → API.
Todas las respuestas son JSON. Los recursos se leen con GET, y con PATCH se modifican sus campos externos, siempre que la key tenga el permiso del recurso. Cualquier otro método, o una ruta que no existe, responde 404 con el código not_found.
Autenticación
Cada llamada lleva la key en el encabezado Authorization, con el esquema Bearer:
Authorization: Bearer spk_…No se acepta la key en la dirección ni en otro esquema. Una llamada sin key, con una key revocada, con una key de otra cuenta o con una key que nunca existió responde lo mismo: 401 con el código unauthorized. La respuesta no distingue entre esos casos a propósito.
Una cuenta desactivada también responde 401.
Permisos
Cada key tiene permisos por recurso, que se eligen al emitirla y no cambian después. Para cada recurso, la key tiene uno de tres niveles:
| Nivel | Permiso | Qué deja hacer |
|---|---|---|
| Sin acceso | — | Nada: toda llamada al recurso responde 403. |
| Lectura | <recurso>:read, por ejemplo sites:read | Leerlo con GET. |
| Lectura y escritura | <recurso>:write, por ejemplo sites:write | Leerlo y modificarlo. La escritura incluye la lectura. |
Cada recurso indica en Recursos qué permiso lo lee y cuál lo modifica. Una llamada sin el permiso que necesita responde 403 con el código insufficient_scope, y el mensaje dice qué permiso falta:
json
{
"error": {
"code": "insufficient_scope",
"message": "Esta key no tiene el permiso sites:write."
}
}Ningún permiso se da solo: una key nueva tiene únicamente lo que se le eligió, y un recurso que se agregue a la API más adelante no le llega a ninguna key existente.
Keys anteriores a los permisos
Una key emitida antes de que existieran los permisos conserva exactamente el acceso que tenía, nunca más: lee account, sites, site_groups y professionals, y con PATCH solo modifica sus campos externos. Cualquier otra escritura, aunque el recurso la admita, responde 403 con el código insufficient_scope. Para elegirle otros permisos, emití una key nueva y revocá la anterior.
Fechas y horas
Todos los instantes van en UTC y en formato ISO 8601, por ejemplo 2026-09-23T17:41:48Z, aunque la cuenta opere en otra zona horaria.
Errores
Todo error responde con el mismo formato: un objeto error con un code en inglés, pensado para que lo lea el sistema, y un message en español, pensado para que lo lea una persona.
json
{
"error": {
"code": "invalid_parameter",
"message": "per_page debe ser un entero entre 1 y 100."
}
}| Estado HTTP | code | Cuándo |
|---|---|---|
400 | idempotency_key_missing | La escritura exige el encabezado Idempotency-Key y no llegó. |
400 | idempotency_key_invalid | El encabezado Idempotency-Key está vacío, supera los 255 caracteres o tiene caracteres no admitidos. |
401 | unauthorized | Falta la key, no es válida para esta cuenta o la cuenta está desactivada. |
403 | module_disabled | La key es válida, pero la cuenta no tiene la API habilitada. |
403 | insufficient_scope | La key es válida, pero no tiene el permiso que la llamada necesita. El mensaje dice cuál. |
404 | not_found | El registro no existe en esta cuenta, o la ruta no existe. |
409 | idempotency_key_in_progress | Otro pedido con la misma Idempotency-Key todavía está en curso. |
422 | invalid_parameter | Un parámetro o un campo enviado con PATCH tiene un valor inválido. El mensaje dice cuál y qué valores acepta. |
422 | idempotency_key_reused | La Idempotency-Key ya se usó con un pedido distinto. |
429 | rate_limited | La key superó su límite de uso. |
La key se valida antes que todo lo demás: una llamada sin key válida recibe 401 aunque la ruta no exista o la cuenta no tenga el módulo habilitado. Después se revisa el módulo (module_disabled) y luego el permiso (insufficient_scope): una key sin permiso sobre un recurso recibe 403 aunque el registro pedido no exista.
Un registro de otra cuenta responde 404, igual que uno que no existe.
Paginación
Los listados se entregan por páginas, ordenados por id de menor a mayor. Así una página no cambia entre dos llamadas seguidas.
| Parámetro | Valor por defecto | Valores válidos |
|---|---|---|
page | 1 | Un entero desde 1. |
per_page | 25 | Un entero entre 1 y 100. |
La respuesta trae los registros en data y la información para recorrer el resto en meta:
json
{
"data": [ … ],
"meta": {
"page": 1,
"per_page": 25,
"total": 42,
"next_page": 2
}
}totales la cantidad de registros que cumplen los filtros, sumando todas las páginas.next_pagees el número de la página siguiente, onullen la última. Para recorrer un listado completo, pedí páginas hasta quenext_pageseanull.- Una página más allá de la última responde con
datavacío, no con un error.
Filtros
Los filtros se pasan como parámetros en la dirección y se combinan con la paginación. Cada recurso indica cuáles acepta.
updated_since
Devuelve solo los registros que cambiaron en o después del instante indicado. Es la forma de sincronizar sin volver a leer todo: guardá el momento de la última lectura y pedí lo que cambió desde entonces.
- Acepta un instante ISO 8601 (
2026-09-01T12:00:00Z) o una fecha sola (2026-09-01), que se toma como la medianoche UTC de ese día. - Un instante sin zona horaria se interpreta en UTC.
- Cualquier otro valor responde
422.
Un registro también cuenta como cambiado cuando cambia algo que la API muestra dentro de él, por ejemplo, un profesional que se asigna a un sitio. Cada recurso detalla qué cambios lo mueven.
status
Filtra por estado. Si no se indica, devuelve solo los registros activos, tal como los muestra el panel. Un valor que el recurso no admite responde 422 y el mensaje lista los válidos.
Una búsqueda por external_reference, o por dni en los empleados, devuelve todos los estados salvo que se indique status.
El filtro solo afecta a los listados: un registro pedido por su id se devuelve en cualquier estado, y el campo status indica en cuál está.
external_reference
Devuelve los registros cuya referencia externa es exactamente el valor indicado. Puede haber cero, uno o varios: Safety Panel no exige que la referencia sea única.
Cuando se busca por referencia, el listado incluye los registros en cualquier estado, no solo los activos, para que un sistema encuentre también a quien está dado de baja. Si se indica status además, se respeta.
Campos externos
Todos los recursos tienen dos campos que pertenecen al sistema que se integra, no a Safety Panel. Safety Panel los guarda y los devuelve, pero nunca los interpreta ni los muestra, salvo la referencia externa, que se ve en el panel en solo lectura.
| Campo | Tipo | Qué contiene |
|---|---|---|
external_reference | texto o null | El identificador que tu sistema le da al registro. Hasta 255 caracteres. No tiene que ser único. |
external_metadata | objeto | Pares clave–valor de tu sistema. Por defecto, {}. |
Se modifican con PATCH sobre el registro, enviando solo estos campos en JSON. Cualquier otro campo responde 422, y un campo que no se envía no cambia.
sh
curl -X PATCH "https://tuempresa.safetypanel.com.ar/api/v1/professionals/45" \
-H "Authorization: Bearer spk_…" \
-H "Content-Type: application/json" \
-d '{"external_reference": "E-123", "external_metadata": {"cost_center": "4410"}}'La respuesta es el registro completo, como lo devuelve GET. Un cambio en estos campos mueve el updated_at del registro.
- Referencia externa: un texto la reemplaza, y
""onullla quitan. - Metadatos: se actualizan por clave. Una clave enviada reemplaza su valor y las demás no cambian. Una clave con
""onullse borra, y"external_metadata": ""los borra todos.
Los metadatos admiten hasta 50 claves. Cada clave tiene hasta 40 caracteres y no lleva [ ni ], y cada valor es un texto de hasta 500 caracteres. Un número, un booleano o un objeto como valor responde 422: guardalo como texto.
No guardes datos sensibles en los metadatos
Los metadatos no se cifran y los lee cualquier key de la cuenta con permiso sobre ese recurso. No guardes contraseñas, tokens ni datos personales sensibles.
Idempotencia
Una escritura puede llevar el encabezado Idempotency-Key con una clave que tu sistema genera para esa operación. Si la llamada se corta —un timeout, una caída de red— y no sabés si llegó, reintentala con la misma clave: Safety Panel no la ejecuta dos veces.
sh
curl -X PATCH "https://tuempresa.safetypanel.com.ar/api/v1/professionals/45" \
-H "Authorization: Bearer spk_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a4e-8b0d-4d5e-9a3f-2c7b1e9d0a64" \
-d '{"external_reference": "E-123"}'- Usá una clave nueva por operación, por ejemplo un UUID. La clave tiene entre 1 y 255 caracteres ASCII visibles, sin espacios.
- Un reintento con la misma clave y el mismo pedido (mismo método, misma ruta con los mismos parámetros y mismo cuerpo) no se vuelve a ejecutar: responde el mismo estado y el mismo cuerpo que la primera vez, con el encabezado
Idempotent-Replayed: true. El orden de los campos en el JSON no importa. - La misma clave con otro pedido responde
422con el códigoidempotency_key_reused. - Si el primer pedido todavía está en curso, el reintento responde
409con el códigoidempotency_key_in_progress. Esperá unos segundos y reintentá con la misma clave. - Solo se recuerdan las respuestas exitosas (
2xx). Si la llamada responde un error, no se escribió nada: corregí el pedido y reintentá, con la misma clave o con otra. - Cada clave vale para la key de API que la usó y se recuerda durante 24 horas. Después, la misma clave se toma como una operación nueva. Dos keys de la misma cuenta no comparten claves.
La autenticación, la habilitación del módulo y los permisos de la key se verifican antes que la clave: una llamada rechazada por cualquiera de ellos no consume la clave ni recibe una respuesta de idempotencia.
Los PATCH de campos externos aceptan el encabezado pero no lo exigen. Las escrituras que lo exigen lo indican en Recursos, y sin él responden 400 con el código idempotency_key_missing.
Límites de uso
Cada key tiene un cupo de 300 llamadas por minuto. El cupo es por key, no por dirección IP, así que dos sistemas con keys distintas no se afectan entre sí.
Cada respuesta a una llamada con key informa el cupo en tres encabezados:
| Encabezado | Qué indica |
|---|---|
x-ratelimit-limit | Las llamadas permitidas por minuto. |
x-ratelimit-remaining | Las que quedan en el minuto en curso. |
x-ratelimit-reset | El instante en que se renueva el cupo, en segundos desde el 1 de enero de 1970 (UTC). |
El cupo se renueva al comienzo de cada minuto, no un minuto después de la primera llamada. Una llamada por encima del cupo responde 429 con el código rate_limited y el encabezado retry-after, que indica cuántos segundos faltan para que se renueve. Esperá ese tiempo antes de reintentar.