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/worktimes

BASE_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.

Licencia
Codenica API
Número máximo de claves
Starter
no disponible
0
Plus
disponible
50
Enterprise
disponible
100

El 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/json

La 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" | jq

Comprueba 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.

targetDataSet
targetItemType
relationshipType
tickets
ticket
parent
changes
change
parent
problems
problem
parent
releases
release
parent

No 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.

Campo
Tipo
Uso
customId
string
Identificador de la integración externa, máximo 500 caracteres.
date
date-time
Fecha o momento del trabajo en formato ISO 8601.
location
string
Lugar donde se realizó el trabajo, máximo 300 caracteres.
department
string
Departamento o unidad responsable, máximo 300 caracteres.
time
integer
Duración en segundos, valor cero o superior.
title
string
Descripción de la sesión o actividad, máximo 1000 caracteres.
category
string
Categoría de facturación o seguimiento, máximo 300 caracteres.
isBillable
boolean
Indica si el tiempo puede facturarse.
{
  "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_required

Despué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}/pin

Tratar 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

HTTP
Código
Significado y respuesta
400
validation_failed
Campo, registro principal o Work Task ya ocupado no válido. Corrige los datos.
401
authentication_required
Comprueba los dos encabezados de la clave y la dirección de la instalación.
403
public_api_scope_denied
Añade el scope necesario a la clave en Configuración -> API.
403
workTime_parent_access_denied
Usa un registro principal accesible para el caller.
404
workTime_not_found
Comprueba el UUID, la dirección y el acceso al registro.
409
idempotency_conflict
Esta clave de idempotencia se utilizó con otro body.
412
if_match_failed
Lee el registro y usa un ETag nuevo.
428
if_match_required
Añade el valor actual de If-Match.
428
idempotency_key_required
Añade un Idempotency-Key único a la mutación.
429
rate_limit_exceeded
Aplica backoff y respeta los encabezados del límite.
503
tenant_context_unavailable
Reintenta con backoff sin cambiar el body.

Despué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.