Skip to navigation

Enviar marcajes de asistencia

Cómo mandar las entradas, descansos y salidas de tu reloj biométrico o de tu sistema de control de acceso.

Si tu organización usa Asistencia en Crehana, cada marca que registra un reloj biométrico o tu propio sistema de acceso se puede enviar por API. Una vez recibida, la marca se procesa igual que una hecha desde la app: se asigna al turno del día de la persona, se clasifica por puntualidad y entra en la gestión diaria.

POST /api/v5/rest/org/{organization_slug}/time/attendance/marks/

La referencia completa está en Create Attendance Mark.

Antes de empezar

  • Credenciales de administrador. Las credenciales de la API tienen que ser de una cuenta ADMIN de la organización. Con otras, la respuesta es 403.
  • Las personas tienen que estar dadas de alta en Asistencia. Si alguien no tiene su configuración de marcaje en la organización, su marca se rechaza con 404. Se da de alta desde Crehana, en Asistencia.
  • El id de cada persona. user_id es el id centralizado: el user.id de GET /org/{organization_slug}/users/user-organizations/. No es el id de learning (learning_user_id). Conviene guardarlo en tu sistema junto al legajo o al id que usa el reloj.

Una marca, de punta a punta

curl -X POST "https://www.crehana.com/api/v5/rest/org/acme/time/attendance/marks/" \
-H "Api-Key: $API_KEY" \
-H "Secret-Access: $SECRET_ACCESS" \
-H "Content-Type: application/json" \
-d '{
"user_id": 97099,
"mark_type": "ENTRY",
"marked_at": "2026-10-07T09:00:00-06:00",
"timezone": "America/Mexico_City"
}'

Respuesta 201:

{
"success": true,
"message": "Mark created.",
"code": "attendance-001",
"datetime": "2026-10-07T09:00:03.120394",
"data": {
"id": "4b2b7d0e-6a3a-4f0e-9a8e-2f6c1d9b7c11",
"user_id": 97099,
"mark_type": "ENTRY",
"mark_date": "2026-10-07",
"marked_at": "2026-10-07T15:00:00+00:00",
"timezone": "America/Mexico_City",
"status": "ON_TIME",
"shift_slot": 1,
"source": "BIOMETRIC",
"already_registered": false
}
}

marked_at vuelve en UTC: es el mismo instante que mandaste. mark_date es el día local, calculado en timezone.

Los cuatro campos

Los cuatro son obligatorios y no se aceptan campos de más.

CampoQué esEjemplo
user_idId centralizado de la persona97099
mark_typeENTRY, BREAK_START, BREAK_END o EXIT, en mayúsculas"ENTRY"
marked_atFecha y hora reales de la marca, ISO 8601 con desfase o en UTC con Z"2026-10-07T08:58:00-06:00"
timezoneZona IANA del lugar donde se marcó"America/Mexico_City"

mark_type

Un día normal va en este orden:

ENTRY → BREAK_START → BREAK_END → EXIT
ValorSignificaNecesita antes
ENTRYEmpieza la jornada o el turnoNada
BREAK_STARTSale al descansoUna ENTRY abierta, y que el turno tenga descanso
BREAK_ENDVuelve del descansoUn BREAK_START abierto
EXITTermina la jornada o el turnoUna ENTRY abierta

Si tu reloj no distingue el tipo de marca, tu integración tiene que decidirlo antes de enviar (por ejemplo, la primera del día es ENTRY y la siguiente EXIT). La API no lo adivina.

Si quedó abierta una jornada anterior (una ENTRY sin EXIT), la siguiente ENTRY la cierra con una salida de sistema y recalcula ese día.

marked_at

  • Es la hora en que la persona marcó, no la hora en que la enviás. Si el reloj estuvo sin conexión y manda las marcas horas después, cada una conserva su hora original.
  • Tiene que llevar desfase (-06:00) o Z. Sin desfase la hora es ambigua y se rechaza:
    • ✅ 2026-10-07T08:58:00-06:00
    • ✅ 2026-10-07T14:58:00Z, el mismo instante en UTC
    • ❌ 2026-10-07T08:58:00
    • ❌ 2026-10-07 08:58, 07/10/2026 08:58 o un timestamp numérico
  • No puede estar en el futuro. Se toleran dos minutos por diferencias de reloj del dispositivo.
  • Se evalúa al minuto: los segundos no cuentan.

timezone

La zona IANA de la sede o del dispositivo, por ejemplo America/Lima. Decide a qué día pertenece la marca y contra qué horario se compara la puntualidad. Lo que sirve y lo que no, y la lista por país, están en Zonas horarias.

Una zona equivocada no da error: deja la marca en otro día o la clasifica mal. Una salida a las 23:30 del 6 en Ciudad de México enviada con "UTC" queda en el día 7.

Reintentos: mandar dos veces es seguro

Si se corta la conexión y no sabés si la marca llegó, reenviala igual. Si ya existe una marca del mismo tipo, misma persona y mismo minuto, la respuesta es 200 con already_registered: true y los datos de la marca guardada. No se crea un duplicado.

{
"success": true,
"message": "Mark already registered.",
"code": "attendance-002",
"data": { "already_registered": true, "...": "..." }
}

Evitá reenviar en paralelo: si dos copias llegan exactamente a la vez, una se guarda y la otra responde 409 (INTEGRITY_ERROR).

Es distinto de mandar otra hora para un tipo que el turno ya tiene: una segunda ENTRY a las 09:15 cuando ya hay una a las 08:58 es un 409. Esa hora no se corrige por la API: se ajusta desde la edición de la jornada en el panel de Crehana.

Respuestas

StatusCuándoQué hacer
201Marca creada—
200Ya se había enviado (already_registered: true)Nada, ya está
400La marca no respeta las reglas del día, o el organization_slug no existeVer el código en detail.code. Reintentar igual no sirve
401Faltan las credenciales o no son válidasRevisar los dos headers
403Las credenciales no administran esa organizaciónUsar credenciales de un admin de la organización
404La persona no está dada de alta en Asistencia en la organización, o no pertenece a ellaRevisar el user_id, darla de alta
409El turno ya tiene una marca de ese tipo a otra hora, o la misma marca llegó dos veces a la vezRevisar las marcas del día en Crehana; no reenviar
422El cuerpo no cumple el esquema (falta un campo, timezone o marked_at mal formados, mark_type desconocido)Corregir el campo que indica detail[].loc
500Error nuestroReintentar con backoff

Los 400, 403, 404 y 409 traen el motivo en detail:

{
"detail": {
"errors": ["Cannot register this mark: entry has not been recorded yet"],
"code": "NO_ACTIVE_ENTRY"
}
}
detail.codeStatusSignifica
NO_ACTIVE_ENTRY400EXIT o BREAK_START sin una ENTRY abierta en ese turno
NO_ACTIVE_BREAK400BREAK_END sin un BREAK_START abierto
INVALID_MARK_TYPE_FOR_SHIFT400El turno de la persona no tiene descanso y se mandó BREAK_START o BREAK_END
MARK_WITHIN_SAME_MINUTE400Otra marca de la persona en el mismo minuto
EXIT_BEFORE_ENTRY400La salida tiene una hora anterior a la entrada del turno
MARK_ORDER_INVALID400La hora no respeta el orden del día (por ejemplo, un fin de descanso antes de su inicio)
MARKED_AT_IN_FUTURE400marked_at es posterior a la hora actual
MARK_ALREADY_REGISTERED409Ya hay una marca de ese tipo en ese turno, a otra hora
INTEGRITY_ERROR409La misma marca llegó dos veces al mismo tiempo (dos reintentos en paralelo). Una de las dos quedó guardada
USER_NOT_FOUND404La persona no está dada de alta en Asistencia en la organización
FORBIDDEN403Las credenciales no administran la organización

Los textos de errors son para un log o para una persona: programá contra el status y, si hace falta, contra detail.code. Más en Errores.

Enviar varias marcas

El endpoint recibe una marca por llamada. Para un lote (por ejemplo, lo que juntó un reloj sin conexión), mandalas en orden cronológico por persona: la EXIT antes que la ENTRY del día siguiente. Fuera de orden, una EXIT puede llegar antes que su ENTRY y rechazarse con NO_ACTIVE_ENTRY.

import requests
BASE = "https://www.crehana.com/api/v5/rest/org/acme/time/attendance/marks/"
HEADERS = {"Api-Key": API_KEY, "Secret-Access": SECRET_ACCESS}
for mark in sorted(pending_marks, key=lambda m: (m["user_id"], m["marked_at"])):
response = requests.post(BASE, json=mark, headers=HEADERS, timeout=30)
if response.status_code in (200, 201):
mark_as_sent(mark)
elif response.status_code >= 500:
retry_later(mark) # con backoff; reenviar es seguro
else:
log_rejected(mark, response.json()) # 4xx: reintentar igual no cambia nada

Lista de verificación

  • Las credenciales son de un ADMIN de la organización.
  • Cada persona está dada de alta en Asistencia y tenés su user_id centralizado.
  • marked_at es la hora real de la marca y lleva desfase o Z.
  • timezone es la zona IANA de la sede, con mayúsculas exactas.
  • Los lotes van en orden cronológico por persona.
  • Los 500 y los cortes de red se reintentan. Los 4xx se registran y no se reintentan.