Horas de trabajo - en Codenica API
Antes de enviar la primera solicitud de horas de trabajo, crea una clave API en los ajustes de tu instalación. Si todavía no existe ninguna, abre en una pestaña nueva la página Codenica API - Introducción. Allí se explican las reglas comunes para crear claves, autenticar solicitudes, elegir la dirección de la API y guardar el secreto.
Una hora de trabajo registra el tiempo dedicado a un Ticket, Change, Problem o Release concreto. Es un objeto deliberadamente limitado: no tiene archivos propios, pin ni un catálogo libre de relaciones. Puede tener exactamente un registro principal, un Work Task opcional y un agente opcional.
En el contrato técnico, la colección se denomina worktimes y cada registro utiliza worktime en itemType.
Horas de trabajo - dirección de la API y elección de la instalación
Todas las rutas de las horas de trabajo comienzan por:
{BASE_URL}/api/v1/worktimesBASE_URL incluye el protocolo y el host de la aplicación, pero no el /api/v1 final.
Codenica Cloud: usa el dominio o subdominio real asignado a la empresa.
export BASE_URL="https://{company-domain}"Codenica On-Premise: Codenica Discovery registra localmente por defecto la dirección http://codenica.local:5150.
Si el administrador publica la instalación mediante un dominio corporativo, HTTPS, reverse proxy u otro puerto, usa la dirección exacta facilitada para esa instalación. No supongas que una instalación On-Premise debe usar localhost. Ese nombre identifica el ordenador que ejecuta el cliente HTTP, no necesariamente el servidor de Codenica.
export BASE_URL="http://codenica.local:5150"Si un proxy inverso o el administrador proporciona otra dirección, usa exactamente esa dirección.
La dirección http://localhost:5050 solo sirve para el desarrollo local cuando la API se ejecuta en el mismo ordenador. No es la dirección estándar de Cloud ni la dirección predeterminada de On-Premise.
Horas de trabajo - clave API y límites de licencia
Crea la clave en Configuración -> API -> API Keys. Una clave independiente para cada aplicación y entorno facilita la rotación, la auditoría y la retirada de un único acceso.
StarterPlusEnterpriseEl secreto solo se muestra al crear o rotar la clave. Guarda Client ID y Client Secret en un almacén seguro de secretos. Al eliminar la clave se elimina su registro y se libera su espacio del límite de licencia.
Horas de trabajo - autenticación de las solicitudes
Autentica cada solicitud de Codenica API con dos encabezados:
X-Codenica-Client-Id: {CLIENT_ID}
X-Codenica-Client-Secret: {CLIENT_SECRET}
Accept: application/jsonLa integración externa no necesita una sesión del panel ni el Bearer JWT del usuario. Guarda el secreto en el servidor o en un gestor de secretos.
export CLIENT_ID="cna_your_client_id"
export CLIENT_SECRET="cns_your_client_secret"
curl --fail-with-body --silent --show-error \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/context"No incluyas el secreto en código del navegador, repositorios, URL, historial de comandos ni registros.
Horas de trabajo - comprobar el contexto de conexión
Lee el contexto antes de descargar una lista o registrar tiempo. Así confirmas que la dirección conduce a la base de datos correcta y que la clave tiene los scopes y límites necesarios.
curl --fail-with-body --silent --show-error \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/context" | jqComprueba data.tenant.id, data.tenant.name, data.tenant.resolvedDomain, data.caller.clientId y data.caller.scopes. data.caller.authentication debe ser api_key. Comprueba también data.capabilities.supportsETag, supportsIdempotency y supportsRelationships.
{
"data": {
"apiVersion": "v1",
"caller": {
"authentication": "api_key",
"clientId": "{CLIENT_ID}",
"scopes": [
"worktimes:read",
"worktimes:write"
]
},
"capabilities": {
"supportsETag": true,
"supportsIdempotency": true,
"supportsRelationships": true
}
},
"meta": {
"requestId": "{REQUEST_ID}"
}
}Si el contexto muestra otra empresa o falta un scope necesario, corrige la dirección o crea una clave con los permisos adecuados. No intentes seleccionar la base de datos añadiendo otro identificador en el body.
Horas de trabajo - esquema y campos compatibles
El esquema es la referencia del contrato actual de las horas de trabajo. Devuelve tipos de campos, posibilidad de escritura, límites, campos técnicos y destinos de relación permitidos.
curl --fail-with-body --silent --show-error \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes/schema" | jq{
"data": {
"itemType": "worktime",
"fields": [
{ "name": "date", "type": "dateTime", "writable": true },
{ "name": "time", "type": "integer", "writable": true },
{ "name": "isBillable", "type": "boolean", "writable": true },
{ "name": "dateCreated", "type": "dateTime", "writable": false, "system": true }
],
"relationshipTargets": [
{ "targetDataSet": "tickets", "targetItemType": "ticket" },
{ "targetDataSet": "changes", "targetItemType": "change" },
{ "targetDataSet": "problems", "targetItemType": "problem" },
{ "targetDataSet": "releases", "targetItemType": "release" },
{ "targetDataSet": "worktasks", "targetItemType": "worktask" }
],
"userRelationshipTypes": ["agent"]
}
}Antes de mapear los campos, comprueba readable, writable, required, technical y maxLength. No construyas la integración únicamente a partir de una respuesta de ejemplo.
Horas de trabajo - registro principal y visibilidad
Cada hora de trabajo debe tener exactamente un registro principal operativo. Puede ser un Ticket, Change, Problem o Release.
ticketsticketparentchangeschangeparentproblemsproblemparentreleasesreleaseparentNo se puede crear el registro sin esta relación y tampoco se puede eliminar el último registro principal de un registro existente. La visibilidad depende del acceso al registro principal. Tener acceso solo al Work Task no basta para que la hora aparezca en la lista.
Envía la relación principal en el array relationships. No escribas ticketId, changeId, problemId ni releaseId dentro de attributes.
Horas de trabajo - campos editables y del sistema
Estos campos transmiten los datos de negocio dentro de attributes. Si el esquema de la base de datos actual indica otros límites, prevalece el esquema.
customIddatelocationdepartmenttimetitlecategoryisBillable{
"date": "2026-09-06T09:00:00Z",
"time": 5400,
"title": "Atención de una incidencia por el Service Desk",
"category": "Service Desk",
"isBillable": true
}Ejemplo de una entrada de 90 minutos:
Los campos isAuto, agentId, workTaskId, ticketId, changeId, problemId, releaseId, creator, updater, dateCreated, dateUpdated, importId, importSource y dateImported son técnicos o del sistema. No los escribas en attributes; gestiona el agente y el Work Task mediante sus endpoints de relación.
Horas de trabajo - endpoints principales
La colección de horas de trabajo ofrece las siguientes rutas:
GET /api/v1/worktimes
POST /api/v1/worktimes
POST /api/v1/worktimes:batch
GET /api/v1/worktimes/schema
GET /api/v1/worktimes/stats
GET /api/v1/worktimes/values
GET /api/v1/worktimes/{WORKTIME_ID}
PATCH /api/v1/worktimes/{WORKTIME_ID}
DELETE /api/v1/worktimes/{WORKTIME_ID}
GET /api/v1/worktimes/{WORKTIME_ID}/relationships
POST /api/v1/worktimes/{WORKTIME_ID}/relationships
POST /api/v1/worktimes/{WORKTIME_ID}/relationships:batch
DELETE /api/v1/worktimes/{WORKTIME_ID}/relationships/{DATASET}/{TARGET_ID}
GET /api/v1/worktimes/{WORKTIME_ID}/user-relationships
POST /api/v1/worktimes/{WORKTIME_ID}/user-relationships
POST /api/v1/worktimes/{WORKTIME_ID}/user-relationships:batch
DELETE /api/v1/worktimes/{WORKTIME_ID}/user-relationships/{TARGET_ID}Cada lectura requiere el scope read del grupo correspondiente. Cada mutación requiere además un Idempotency-Key único. El ETag actual es obligatorio para editar, cambiar relaciones y eliminar.
Horas de trabajo - listado y paginación
Lee la lista por páginas. Define el orden explícitamente para que las lecturas posteriores mantengan un orden predecible:
curl --fail-with-body --silent --show-error -G \
--data-urlencode "itemType=worktime" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=25" \
--data-urlencode "sort=date" \
--data-urlencode "direction=desc" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes"La respuesta contiene data.items y la información de la página.
{
"data": {
"items": [
{
"id": "{WORKTIME_ID}",
"itemType": "worktime",
"attributes": {
"customId": "ERP-WORKTIME-2026-0042",
"date": "2026-09-06T09:00:00Z",
"time": 5400,
"title": "Atención de una incidencia por el Service Desk",
"isBillable": true
},
"meta": { "etag": "\"{ETAG}\"" }
}
],
"page": 1,
"pageSize": 25,
"totalItems": 1,
"totalPages": 1,
"hasNextPage": false
},
"meta": { "requestId": "{REQUEST_ID}" }
}Solicita la página siguiente solo cuando hasNextPage sea true. Consulta el pageSize máximo en los límites devueltos por el contexto.
Horas de trabajo - búsqueda y filtros
Usa search para una búsqueda de texto sencilla. Para sincronizar, es preferible utilizar un customId estable, un UUID o un filtro por el registro principal:
curl --fail-with-body --silent --show-error -G \
--data-urlencode "search=Service Desk" \
--data-urlencode "category=Service Desk" \
--data-urlencode "isBillable=true" \
--data-urlencode "parentDataSet=tickets" \
--data-urlencode "parentId=$TICKET_ID" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=50" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes"Entre los parámetros disponibles están customId, date, dateAfter, dateBefore, location, department, time, title, category, isBillable, agentId, createdAfter, createdBefore, updatedAfter, updatedBefore, sort y direction. El filtro parentDataSet también requiere parentId.
time:gte:3600
time:lt:28800
category:eq:Service Desk
title:contains:incidencia
customId:startswith:ERP-WORKTIME-
location:notempty:Los filtros estructurales usan el formato field:operator:value:
Los operadores compatibles son eq, ne, gt, gte, lt, lte, contains, startswith, endswith y notempty. Codifica en la URL los valores que contengan espacios, dos puntos o caracteres especiales.
Horas de trabajo - selección de campos y datos incluidos
Usa fields cuando la respuesta deba contener solo los datos necesarios para la sincronización. Incluye las relaciones de objetos y usuarios mediante include:
curl --fail-with-body --silent --show-error -G \
--data-urlencode "fields=id,itemType,customId,date,time,title,category,isBillable" \
--data-urlencode "include=relationships,users" \
--data-urlencode "ids=$WORKTIME_ID" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes"En las horas de trabajo, los valores compatibles de include son relationships y users. Work Times no admite include=files. Los campos técnicos requieren el scope técnico correspondiente y fields=* no evita los permisos ni expone los campos del sistema de la envoltura de respuesta.
Horas de trabajo - estadísticas y valores de campos
Las estadísticas permiten comprobar rápidamente la distribución de los datos sin descargar toda la colección. Este ejemplo agrupa por categoría:
curl --fail-with-body --silent --show-error -G \
--data-urlencode "field=category" \
--data-urlencode "limit=20" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes/stats"{
"data": {
"total": 11,
"field": "category",
"values": [
{ "value": "Service Desk", "count": 7 },
{ "value": "Public API", "count": 4 }
]
},
"meta": { "requestId": "{REQUEST_ID}" }
}Para obtener valores que coincidan con una búsqueda, usa values:
curl --fail-with-body --silent --show-error -G \
--data-urlencode "field=category" \
--data-urlencode "search=Public" \
--data-urlencode "limit=20" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes/values"Las estadísticas y los valores son operaciones de solo lectura. No modifican los registros de horas de trabajo.
Horas de trabajo - creación mínima
Una creación mínima necesita itemType, campos en attributes y exactamente una relación parent. Cada solicitud POST debe incluir un Idempotency-Key nuevo:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "Idempotency-Key: erp-worktime-create-0001" \
--data '{
"itemType": "worktime",
"attributes": {
"customId": "ERP-WORKTIME-2026-0042",
"date": "2026-09-06T09:00:00Z",
"time": 5400,
"title": "Atención de una incidencia por el Service Desk",
"category": "Service Desk",
"isBillable": true
},
"relationships": [
{
"targetId": "{TICKET_ID}",
"targetDataSet": "tickets",
"targetItemType": "ticket",
"relationshipType": "parent"
}
]
}' \
"$BASE_URL/api/v1/worktimes"La respuesta esperada suele ser HTTP 201 Created. Guarda data.id y el ETag del encabezado HTTP y de data.meta.etag.
Horas de trabajo - creación con ubicación y agente
Los campos de negocio adicionales, como ubicación y departamento, pueden enviarse en la misma solicitud. Un agente es una relación de usuario y debe ir en el array separado userRelationships:
{
"itemType": "worktime",
"attributes": {
"customId": "ERP-WORKTIME-2026-0043",
"date": "2026-09-06T10:30:00Z",
"location": "Cracovia",
"department": "IT",
"time": 1800,
"title": "Análisis de un problema y contacto con el usuario",
"category": "Operaciones",
"isBillable": false
},
"relationships": [
{
"targetId": "{PROBLEM_ID}",
"targetDataSet": "problems",
"targetItemType": "problem",
"relationshipType": "parent"
}
],
"userRelationships": [
{
"targetId": "{APP_USER_ID}",
"targetDataSet": "users",
"relationshipType": "agent"
}
]
}El targetId del agente es el identificador de un usuario activo de la aplicación. No uses aquí el identificador de un cliente de la colección clients. Una hora de trabajo puede tener como máximo un agente.
Horas de trabajo - Work Task opcional
Puedes añadir un Work Task como relación de objeto adicional. No sustituye al registro principal:
"relationships": [
{
"targetId": "{TICKET_ID}",
"targetDataSet": "tickets",
"targetItemType": "ticket",
"relationshipType": "parent"
},
{
"targetId": "{WORKTASK_ID}",
"targetDataSet": "worktasks",
"targetItemType": "worktask",
"relationshipType": "worktask"
}
]Un Work Task solo puede asignarse a una hora de trabajo. Si ya está ocupado, la API devuelve HTTP 400, el código validation_failed y el mensaje The WorkTask is already assigned to another WorkTime.. Selecciona un Work Task libre o no envíes esta relación. No cambies el campo técnico workTaskId dentro de attributes.
Horas de trabajo - Idempotency-Key y reintentos seguros
La idempotencia evita duplicados cuando el cliente no recibe la respuesta a tiempo. Al reintentar, envía exactamente el mismo body con la misma clave:
curl --fail-with-body --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "Idempotency-Key: erp-worktime-create-0001" \
--data-binary @worktime-create.json \
"$BASE_URL/api/v1/worktimes"La misma clave y el mismo body deben devolver el mismo recurso, no crear otro registro. Reutilizar la clave con un body diferente produce 409 idempotency_conflict. Genera una clave nueva para cada operación nueva.
Horas de trabajo - leer un registro
Después de crear el registro, léelo mediante el UUID devuelto. Puedes incluir el registro principal, el Work Task y el agente con los parámetros include:
curl --fail-with-body --silent --show-error \
--request GET \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID?itemType=worktime&include=relationships,users" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET"El resultado normal es HTTP 200 OK. Comprueba data.attributes.time, date, title, category e isBillable, además de data.relationships, data.userRelationships y data.meta.etag.
Horas de trabajo - actualizar con ETag e If-Match
Lee un ETag nuevo antes de cambiar un registro. Actualiza solo los campos necesarios y envía ese ETag en el encabezado If-Match:
curl --fail-with-body --silent --show-error \
--request PATCH \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-update-0001" \
--data '{
"attributes": {
"time": 7200,
"title": "Atención de una incidencia - segunda sesión",
"isBillable": false
}
}'Después de HTTP 200 OK, guarda el ETag nuevo. No reutilices el valor anterior en la siguiente modificación.
Horas de trabajo - ETag obsoleto o ausente
El control de versiones evita que una integración sobrescriba un cambio guardado por otra persona o sistema. Si omites If-Match, la API devuelve HTTP 428 Precondition Required con if_match_required. Un ETag antiguo devuelve HTTP 412 Precondition Failed con if_match_failed:
HTTP/1.1 412 Precondition Failed
code: if_match_failed
HTTP/1.1 428 Precondition Required
code: if_match_requiredDespués de un 412, vuelve a leer el registro, compara los datos actuales con el cambio previsto y decide si debes enviar otro PATCH. No uses un bucle de reintentos ciego. Un cambio rechazado no debe modificar el tiempo, el registro principal ni las relaciones.
Horas de trabajo - relaciones de objeto con Work Task
Usa las rutas /relationships para las relaciones de objetos. Las horas de trabajo solo admiten destinos tickets, changes, problems, releases y worktasks.
curl --fail-with-body --silent --show-error \
--request POST \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID/relationships" \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-worktask-add-0001" \
--data '{
"targetId": "{WORKTASK_ID}",
"targetDataSet": "worktasks",
"targetItemType": "worktask",
"relationshipType": "worktask"
}'Lee la colección mediante GET /api/v1/worktimes/{WORKTIME_ID}/relationships. Para eliminar una relación necesitas el ETag actual:
curl --fail-with-body --silent --show-error \
--request DELETE \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID/relationships/worktasks/$WORKTASK_ID?relationshipType=worktask" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-worktask-remove-0001"Actualiza el ETag después de cada cambio de relación. Eliminar el Work Task no elimina la hora de trabajo.
Horas de trabajo - cambiar el registro principal
Para trasladar el tiempo de un ticket a un cambio, usa un solo PATCH que elimine el registro principal anterior y añada el nuevo. Después de la operación debe quedar exactamente un registro principal:
{
"relationshipsToRemove": [
{
"targetId": "{OLD_TICKET_ID}",
"targetDataSet": "tickets",
"targetItemType": "ticket",
"relationshipType": "parent"
}
],
"relationshipsToAdd": [
{
"targetId": "{NEW_CHANGE_ID}",
"targetDataSet": "changes",
"targetItemType": "change",
"relationshipType": "parent"
}
]
}Envía la operación a /api/v1/worktimes/{WORKTIME_ID} con el If-Match actual y un Idempotency-Key nuevo. No elimines primero el registro principal anterior en una solicitud independiente, porque el registro quedaría temporalmente sin el vínculo obligatorio.
Horas de trabajo - relación de agente
El agente es el usuario asignado a la hora de trabajo. Usa las rutas /user-relationships, no las de /relationships:
curl --fail-with-body --silent --show-error \
--request POST \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID/user-relationships" \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-agent-add-0001" \
--data '{
"targetId": "{APP_USER_ID}",
"targetDataSet": "users",
"relationshipType": "agent"
}'Lee la lista con GET /api/v1/worktimes/{WORKTIME_ID}/user-relationships?relationshipType=agent. Para cambiar de agente, elimina primero la relación actual y después añade la nueva, usando un ETag nuevo en cada paso. Una hora de trabajo puede tener como máximo un agente.
El targetId debe identificar a un usuario activo y visible de la aplicación. No es Clients.Id ni el identificador de la empresa.
Horas de trabajo - batch de relaciones
Usa relationships:batch cuando una operación deba añadir o eliminar varias relaciones. Las relaciones de agente tienen la ruta equivalente user-relationships:batch:
curl --fail-with-body --silent --show-error \
--request POST \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID/relationships:batch" \
--header "Content-Type: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-relationships-batch-0001" \
--data '{
"add": [
{
"targetId": "{WORKTASK_ID}",
"targetDataSet": "worktasks",
"targetItemType": "worktask",
"relationshipType": "worktask"
}
],
"remove": []
}'La respuesta contiene los contadores added, removed y skipped. Mantén exactamente una relación parent; nunca envíes un batch que elimine el único registro principal.
{
"data": {
"added": 1,
"removed": 0,
"skipped": 0
},
"meta": { "requestId": "{REQUEST_ID}" }
}Comprueba los contadores de la respuesta antes de dar la operación por terminada.
Horas de trabajo - operaciones batch para registros
Para varias horas de trabajo, usa POST /api/v1/worktimes:batch. Cada elemento indica la operación create, update o delete:
{
"items": [
{
"operation": "create",
"create": {
"itemType": "worktime",
"attributes": {
"customId": "ERP-WORKTIME-BATCH-0001",
"date": "2026-09-06T10:00:00Z",
"time": 1800,
"title": "Tiempo importado desde el ERP",
"category": "Public API",
"isBillable": true
},
"relationships": [
{
"targetId": "{TICKET_ID}",
"targetDataSet": "tickets",
"targetItemType": "ticket",
"relationshipType": "parent"
}
]
}
}
]
}Cada elemento update o delete necesita su propio id y ifMatch. Envía ifMatch como una cadena que contenga el ETag actual:
{
"items": [
{
"operation": "update",
"id": "{WORKTIME_ID}",
"ifMatch": "\"{CURRENT_ETAG}\"",
"update": {
"attributes": {
"time": 2700
}
}
},
{
"operation": "delete",
"id": "{OTHER_WORKTIME_ID}",
"ifMatch": "\"{OTHER_CURRENT_ETAG}\""
}
]
}Comprueba cada elemento mediante index, operation y status. Un resultado parcial puede usar HTTP 207 Multi-Status; el fallo de un elemento no confirma el éxito de los demás.
Horas de trabajo - eliminar un registro
La eliminación es irreversible desde el punto de vista de Public API. Antes de hacerlo, lee un ETag nuevo y confirma que el UUID y customId identifican el registro correcto:
curl --fail-with-body --silent --show-error \
--request DELETE \
--url "$BASE_URL/api/v1/worktimes/$WORKTIME_ID" \
--header "Accept: application/json" \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
--header "If-Match: $WORKTIME_ETAG" \
--header "Idempotency-Key: erp-worktime-delete-0001"El resultado correcto es HTTP 200 OK con data=true. Después vuelve a leer el UUID:
curl --fail-with-body --silent --show-error \
--header "X-Codenica-Client-Id: $CLIENT_ID" \
--header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
"$BASE_URL/api/v1/worktimes/$WORKTIME_ID"La lectura posterior debe devolver HTTP 404 Not Found con workTime_not_found. Comprueba también una lista filtrada por customId para confirmar que el registro no vuelve a aparecer.
Horas de trabajo - sin archivos ni pin
Las horas de trabajo no tienen un módulo propio de archivos ni una acción de pin. No uses estas rutas:
/api/v1/worktimes/{WORKTIME_ID}/files
/api/v1/worktimes/{WORKTIME_ID}/pinTratar una hora de trabajo como un objeto con archivos o pin no forma parte del contrato compatible. Si el tiempo necesita un documento o adjunto, guarda el archivo en un objeto que admita archivos, por ejemplo un ticket o documento, y conserva la relación en el sistema de integración.
Horas de trabajo - errores y límites
validation_failedauthentication_requiredpublic_api_scope_deniedworkTime_parent_access_deniedworkTime_not_foundidempotency_conflictif_match_failedif_match_requiredidempotency_key_requiredrate_limit_exceededtenant_context_unavailableDespués de un error, registra el estado HTTP, code y meta.requestId, pero nunca el secreto de la clave. En HTTP 400, revisa también errors, que identifica el campo o elemento del array concreto.
Lee los límites de solicitudes y batch en data.capabilities.limits. Los encabezados X-RateLimit-Limit y X-RateLimit-Remaining ayudan a ajustar la velocidad de sincronización.
Horas de trabajo - sincronización y n8n
Para sincronizar con un ERP, helpdesk o n8n, usa un customId asignado por el sistema externo, como ERP-WORKTIME-{external-id}. No uses el título como clave de deduplicación porque dos sesiones pueden tener la misma descripción.
En n8n basta con un nodo HTTP Request. Guarda X-Codenica-Client-Id, X-Codenica-Client-Secret y Accept: application/json en las credenciales de encabezados. Añade un Idempotency-Key único a las solicitudes POST.
{
"itemType": "worktime",
"attributes": {
"customId": "N8N-WORKTIME-{{$execution.id}}",
"date": "2026-09-06T09:00:00Z",
"time": 1800,
"title": "Tiempo sincronizado mediante n8n",
"category": "Public API",
"isBillable": true
},
"relationships": [
{
"targetId": "{{$json.ticketId}}",
"targetDataSet": "tickets",
"targetItemType": "ticket",
"relationshipType": "parent"
}
]
}En las actualizaciones, el flujo debe leer primero el registro, conservar data.meta.etag y enviar después el PATCH con ese ETag. Con HTTP 412, vuelve a leer y resuelve el conflicto. Con HTTP 429, aplica un backoff limitado. El secreto no debe aparecer en un nodo Code, en los datos de entrada del flujo ni en el historial de ejecuciones.
Horas de trabajo - orden recomendado de operaciones
Un orden seguro para la integración es el siguiente:
1. Establece BASE_URL para Cloud u On-Premise.
2. Crea una clave en Configuración -> API y guarda el secreto.
3. Lee GET /api/v1/context y comprueba la base de datos, los scopes y los límites.
4. Lee GET /api/v1/worktimes/schema.
5. Selecciona un Ticket, Change, Problem o Release disponible.
6. Selecciona opcionalmente un Work Task libre y un agente activo.
7. Crea el registro con un registro principal y un Idempotency-Key nuevo.
8. Conserva el UUID y el ETag de la respuesta.
9. Lee el registro con include=relationships,users.
10. Para modificarlo, usa un If-Match actual y un Idempotency-Key nuevo.
11. Cambia las relaciones mediante el endpoint correspondiente, nunca mediante campos técnicos.
12. En importaciones masivas, comprueba cada elemento del batch.
13. Lee un ETag nuevo antes de DELETE.
14. Confirma HTTP 404 y la ausencia de customId después de DELETE.Las reglas esenciales son sencillas: una hora de trabajo tiene un registro principal, el tiempo se guarda en segundos, el Work Task es opcional y solo puede ocupar un vínculo, el agente es una relación de usuario y no se admiten archivos ni pin. La idempotencia protege la escritura y el ETag protege la versión concreta del registro.
