Heures de travail - dans Codenica API

Avant d'envoyer la première requête concernant les heures de travail, créez une clé API dans les paramètres de votre installation. Si aucune clé n'existe encore, ouvrez dans un nouvel onglet la page Codenica API - Introduction. Elle présente les règles communes de création des clés, d'authentification, de choix de l'adresse API et de conservation du secret.

Une heure de travail enregistre le temps consacré à un ticket, une modification, un problème ou une release précis. Cet objet reste volontairement limité : il ne possède ni fichiers propres, ni épingle, ni catalogue libre de relations. Il peut avoir exactement un parent principal, une Work Task facultative et un Agent facultatif.

Dans le contrat technique, la collection est nommée worktimes et un enregistrement individuel utilise worktime dans itemType.


Heures de travail - adresse de l'API et choix de l'installation

Toutes les routes des heures de travail commencent par :

{BASE_URL}/api/v1/worktimes

BASE_URL contient le protocole et l'hôte de l'application, sans le /api/v1 final.

Codenica Cloud : utilisez le domaine ou le sous-domaine réel attribué à l'entreprise.

export BASE_URL="https://{company-domain}"

Codenica On-Premise : Codenica Discovery enregistre localement par défaut l'adresse http://codenica.local:5150.

Si l'administrateur publie l'installation avec un domaine d'entreprise, HTTPS, un reverse proxy ou un autre port, utilisez l'adresse exacte communiquée pour cette installation. Ne partez pas du principe qu'un utilisateur On-Premise doit utiliser localhost : ce nom désigne l'ordinateur qui exécute le client HTTP, pas nécessairement le serveur Codenica.

export BASE_URL="http://codenica.local:5150"

Si un proxy inverse ou l'administrateur fournit une autre adresse, utilisez exactement cette adresse.

L'adresse http://localhost:5050 est réservée au développement local lorsque l'API fonctionne sur le même ordinateur. Ce n'est ni l'adresse Cloud standard ni l'adresse On-Premise par défaut.


Heures de travail - clé API et limites de licence

Créez la clé dans Paramètres -> API -> API Keys. Une clé séparée pour chaque application et chaque environnement simplifie la rotation, l'audit et la coupure d'un seul accès.

Licence
Codenica API
Nombre maximal de clés
Starter
non disponible
0
Plus
disponible
50
Enterprise
disponible
100

Le secret n'est affiché qu'au moment de la création ou de la rotation. Enregistrez Client ID et Client Secret dans un coffre de secrets. La suppression de la clé supprime son enregistrement et libère sa place dans la limite de licence.


Heures de travail - authentification des requêtes

Authentifiez chaque requête Codenica API avec deux en-têtes :

X-Codenica-Client-Id: {CLIENT_ID}
X-Codenica-Client-Secret: {CLIENT_SECRET}
Accept: application/json

Une intégration externe n'a besoin ni d'une session du panneau ni du Bearer JWT de l'utilisateur. Conservez le secret côté serveur ou dans un gestionnaire de secrets.

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"

Ne placez jamais le secret dans le code du navigateur, un dépôt, une URL, l'historique des commandes ou les journaux.


Heures de travail - vérifier le contexte de connexion

Lisez le contexte avant de charger une liste ou d'enregistrer du temps. Vous vérifiez ainsi que l'adresse mène à la bonne base de données et que la clé possède les scopes et limites nécessaires.

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

Vérifiez data.tenant.id, data.tenant.name, data.tenant.resolvedDomain, data.caller.clientId et data.caller.scopes. data.caller.authentication doit être api_key. Vérifiez aussi data.capabilities.supportsETag, supportsIdempotency et 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 le contexte indique une autre entreprise ou ne contient pas un scope requis, corrigez l'adresse ou créez une clé avec les bons droits. N'essayez pas de choisir la base de données en ajoutant un autre identifiant dans le body.


Heures de travail - schéma et champs pris en charge

Le schéma constitue la référence du contrat actuel des heures de travail. Il renvoie les types, la possibilité d'écriture, les limites, les champs techniques et les cibles de relations autorisées.

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"]
  }
}

Avant de mapper les champs, vérifiez readable, writable, required, technical et maxLength. Ne construisez pas l'intégration uniquement à partir d'une réponse d'exemple.


Heures de travail - parent principal et visibilité

Chaque heure de travail doit avoir exactement un parent opérationnel principal. Ce parent peut être un ticket, une modification, un problème ou une release.

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

La création sans cette relation est refusée et le dernier parent d'un enregistrement existant ne peut pas être supprimé. La visibilité dépend de l'accès au parent principal. L'accès à une Work Task seule ne suffit pas pour afficher l'heure de travail.

Envoyez la relation du parent dans le tableau relationships. N'écrivez pas ticketId, changeId, problemId ou releaseId dans attributes.


Heures de travail - champs modifiables et système

Les champs suivants transmettent les données métier dans attributes. Si le schéma de la base de données actuelle prévoit d'autres limites, c'est lui qui prévaut.

Champ
Type
Utilisation
customId
string
Identifiant de l'intégration externe, 500 caractères maximum.
date
date-time
Date ou moment de l'intervention au format ISO 8601.
location
string
Lieu de l'intervention, 300 caractères maximum.
department
string
Service ou unité responsable, 300 caractères maximum.
time
integer
Durée en secondes, valeur égale ou supérieure à zéro.
title
string
Description de la session ou de l'activité, 1000 caractères maximum.
category
string
Catégorie de facturation ou de reporting, 300 caractères maximum.
isBillable
boolean
Indique si le temps peut être facturé.
{
  "date": "2026-09-06T09:00:00Z",
  "time": 5400,
  "title": "Traitement d'un ticket par le Service Desk",
  "category": "Service desk",
  "isBillable": true
}

Exemple pour une durée de 90 minutes :

Les champs isAuto, agentId, workTaskId, ticketId, changeId, problemId, releaseId, creator, updater, dateCreated, dateUpdated, importId, importSource et dateImported sont techniques ou système. Ne les écrivez pas dans attributes ; gérez l'Agent et la Work Task par leurs endpoints de relation.


Heures de travail - principaux endpoints

La collection des heures de travail expose les routes suivantes :

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}

Chaque lecture nécessite le scope read du groupe concerné. Toute mutation nécessite aussi un Idempotency-Key unique. L'ETag actuel est requis pour modifier, changer une relation ou supprimer.


Heures de travail - liste et pagination

Lisez la liste page par page. Définissez explicitement le tri afin que les lectures suivantes restent prévisibles :

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 réponse contient data.items et les informations de pagination.

{
  "data": {
    "items": [
      {
        "id": "{WORKTIME_ID}",
        "itemType": "worktime",
        "attributes": {
          "customId": "ERP-WORKTIME-2026-0042",
          "date": "2026-09-06T09:00:00Z",
          "time": 5400,
          "title": "Traitement d'un ticket par le Service Desk",
          "isBillable": true
        },
        "meta": { "etag": "\"{ETAG}\"" }
      }
    ],
    "page": 1,
    "pageSize": 25,
    "totalItems": 1,
    "totalPages": 1,
    "hasNextPage": false
  },
  "meta": { "requestId": "{REQUEST_ID}" }
}

Ne demandez la page suivante que si hasNextPage vaut true. Consultez la limite pageSize maximale dans le contexte.


Heures de travail - recherche et filtres

Utilisez search pour une recherche textuelle simple. Pour la synchronisation, préférez un customId stable, un UUID ou un filtre sur le parent 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"

Les paramètres incluent notamment customId, date, dateAfter, dateBefore, location, department, time, title, category, isBillable, agentId, createdAfter, createdBefore, updatedAfter, updatedBefore, sort et direction. Un filtre parentDataSet doit être accompagné de parentId.

time:gte:3600
time:lt:28800
category:eq:Service desk
title:contains:ticket
customId:startswith:ERP-WORKTIME-
location:notempty:

Les filtres structurés utilisent le format field:operator:value :

Les opérateurs pris en charge sont eq, ne, gt, gte, lt, lte, contains, startswith, endswith et notempty. Encodez dans l'URL les valeurs contenant des espaces, des deux-points ou des caractères spéciaux.


Heures de travail - sélection des champs et données incluses

Utilisez fields pour ne renvoyer que les données nécessaires à la synchronisation. Ajoutez les relations d'objets et d'utilisateurs avec 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"

Pour les heures de travail, les valeurs include prises en charge sont relationships et users. include=files n'est pas pris en charge. Les champs techniques nécessitent le scope technique correspondant et fields=* ne contourne ni les permissions ni les champs système de l'enveloppe.


Heures de travail - statistiques et valeurs des champs

Les statistiques donnent une vue rapide de la répartition des données sans télécharger toute la collection. Voici un regroupement par catégorie :

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": "API publique", "count": 4 }
    ]
  },
  "meta": { "requestId": "{REQUEST_ID}" }
}

Pour obtenir les valeurs correspondant à une recherche, utilisez values :

curl --fail-with-body --silent --show-error -G \
  --data-urlencode "field=category" \
  --data-urlencode "search=publique" \
  --data-urlencode "limit=20" \
  --header "X-Codenica-Client-Id: $CLIENT_ID" \
  --header "X-Codenica-Client-Secret: $CLIENT_SECRET" \
  "$BASE_URL/api/v1/worktimes/values"

Les statistiques et les valeurs sont des opérations en lecture seule. Elles ne modifient pas les enregistrements.


Heures de travail - création minimale

Une création minimale nécessite itemType, des champs dans attributes et exactement une relation parent. Chaque requête POST doit avoir un nouvel Idempotency-Key :

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": "Traitement d'un ticket par le Service Desk",
      "category": "Service desk",
      "isBillable": true
    },
    "relationships": [
      {
        "targetId": "{TICKET_ID}",
        "targetDataSet": "tickets",
        "targetItemType": "ticket",
        "relationshipType": "parent"
      }
    ]
  }' \
  "$BASE_URL/api/v1/worktimes"

La réponse attendue est généralement HTTP 201 Created. Conservez data.id ainsi que l'ETag de l'en-tête HTTP et de data.meta.etag.


Heures de travail - création avec emplacement et Agent

Les champs métier supplémentaires, comme le lieu et le service, peuvent être transmis dans la même requête. L'Agent est une relation d'utilisateur et doit figurer dans le tableau séparé userRelationships :

{
  "itemType": "worktime",
  "attributes": {
    "customId": "ERP-WORKTIME-2026-0043",
    "date": "2026-09-06T10:30:00Z",
    "location": "Cracovie",
    "department": "IT",
    "time": 1800,
    "title": "Analyse d'un problème et contact utilisateur",
    "category": "Opérations",
    "isBillable": false
  },
  "relationships": [
    {
      "targetId": "{PROBLEM_ID}",
      "targetDataSet": "problems",
      "targetItemType": "problem",
      "relationshipType": "parent"
    }
  ],
  "userRelationships": [
    {
      "targetId": "{APP_USER_ID}",
      "targetDataSet": "users",
      "relationshipType": "agent"
    }
  ]
}

Le targetId de l'Agent est l'identifiant d'un utilisateur actif de l'application. N'utilisez pas l'identifiant d'un client de la collection clients. Une heure de travail ne peut avoir qu'un seul Agent.


Heures de travail - Work Task facultative

Une Work Task peut être ajoutée comme relation d'objet supplémentaire. Elle ne remplace pas le parent principal :

"relationships": [
  {
    "targetId": "{TICKET_ID}",
    "targetDataSet": "tickets",
    "targetItemType": "ticket",
    "relationshipType": "parent"
  },
  {
    "targetId": "{WORKTASK_ID}",
    "targetDataSet": "worktasks",
    "targetItemType": "worktask",
    "relationshipType": "worktask"
  }
]

Une Work Task ne peut être affectée qu'à une seule heure de travail. Si elle est déjà utilisée, l'API renvoie HTTP 400, le code validation_failed et le message The WorkTask is already assigned to another WorkTime.. Choisissez une Work Task libre ou omettez la relation. Ne modifiez pas le champ technique workTaskId dans attributes.


Heures de travail - Idempotency-Key et relances sûres

L'idempotence évite un double enregistrement lorsque le client ne reçoit pas la réponse à temps. En cas de relance, envoyez exactement le même body avec la même clé :

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 même clé avec le même body doit renvoyer la même ressource, sans créer de doublon. La réutilisation de la clé avec un body différent renvoie 409 idempotency_conflict. Générez une nouvelle clé pour chaque nouvelle opération.


Heures de travail - lire un enregistrement

Après la création, lisez l'enregistrement avec l'UUID retourné. Vous pouvez demander le parent, la Work Task et l'Agent avec les paramètres 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"

Le résultat normal est HTTP 200 OK. Vérifiez data.attributes.time, date, title, category et isBillable, ainsi que data.relationships, data.userRelationships et data.meta.etag.


Heures de travail - modifier avec ETag et If-Match

Lisez un ETag actuel avant de modifier l'enregistrement. N'envoyez que les champs à changer et transmettez cet ETag dans 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": "Traitement d'un ticket - deuxième session",
      "isBillable": false
    }
  }'

Après HTTP 200 OK, enregistrez le nouvel ETag. Ne réutilisez pas l'ancienne valeur pour la modification suivante.


Heures de travail - ETag obsolète ou absent

Le contrôle de version empêche une intégration d'écraser une modification enregistrée par une autre personne ou un autre système. Sans If-Match, l'API renvoie HTTP 428 Precondition Required et if_match_required. Avec un ETag ancien, elle renvoie HTTP 412 Precondition Failed et if_match_failed :

HTTP/1.1 412 Precondition Failed
code: if_match_failed

HTTP/1.1 428 Precondition Required
code: if_match_required

Après un 412, relisez l'enregistrement, comparez les données actuelles avec la modification prévue puis décidez d'un nouveau PATCH. N'utilisez pas une boucle de relance aveugle. Une modification refusée ne doit changer ni la durée, ni le parent, ni les relations.


Heures de travail - relations d'objet avec Work Task

Utilisez les routes /relationships pour les relations d'objets. Les heures de travail acceptent uniquement les cibles tickets, changes, problems, releases et 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"
  }'

Lisez la collection avec GET /api/v1/worktimes/{WORKTIME_ID}/relationships. La suppression d'une relation nécessite l'ETag actuel :

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"

Actualisez l'ETag après chaque modification de relation. Supprimer la Work Task ne supprime pas l'heure de travail.


Heures de travail - changer le parent principal

Pour transférer le temps d'un ticket vers une modification, utilisez un seul PATCH qui retire l'ancien parent et ajoute le nouveau. Il doit rester exactement un parent après l'opération :

{
  "relationshipsToRemove": [
    {
      "targetId": "{OLD_TICKET_ID}",
      "targetDataSet": "tickets",
      "targetItemType": "ticket",
      "relationshipType": "parent"
    }
  ],
  "relationshipsToAdd": [
    {
      "targetId": "{NEW_CHANGE_ID}",
      "targetDataSet": "changes",
      "targetItemType": "change",
      "relationshipType": "parent"
    }
  ]
}

Envoyez l'opération à /api/v1/worktimes/{WORKTIME_ID} avec l'If-Match actuel et un nouvel Idempotency-Key. Ne supprimez pas d'abord l'ancien parent dans une requête séparée, car l'enregistrement serait momentanément dépourvu de parent.


Heures de travail - relation Agent

L'Agent est l'utilisateur affecté à l'heure de travail. Utilisez /user-relationships, et non /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"
  }'

Lisez la liste avec GET /api/v1/worktimes/{WORKTIME_ID}/user-relationships?relationshipType=agent. Pour remplacer l'Agent, supprimez la relation actuelle puis ajoutez la nouvelle, avec un ETag renouvelé à chaque étape. Une heure de travail ne peut avoir qu'un Agent.

Le targetId doit désigner un utilisateur actif et visible de l'application. Ce n'est ni Clients.Id ni l'identifiant de l'entreprise.


Heures de travail - batch de relations

Utilisez relationships:batch lorsqu'une opération doit ajouter ou supprimer plusieurs relations. Les relations d'utilisateurs disposent de la route user-relationships:batch correspondante :

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 réponse contient les compteurs added, removed et skipped. Conservez exactement une relation parent et n'envoyez jamais un batch qui supprimerait le seul parent.

{
  "data": {
    "added": 1,
    "removed": 0,
    "skipped": 0
  },
  "meta": { "requestId": "{REQUEST_ID}" }
}

Vérifiez les compteurs de la réponse avant de considérer l'opération comme terminée.


Heures de travail - opérations batch sur les enregistrements

Pour plusieurs heures de travail, utilisez POST /api/v1/worktimes:batch. Chaque élément indique une opération create, update ou delete :

{
  "items": [
    {
      "operation": "create",
      "create": {
        "itemType": "worktime",
        "attributes": {
          "customId": "ERP-WORKTIME-BATCH-0001",
          "date": "2026-09-06T10:00:00Z",
          "time": 1800,
          "title": "Temps importé depuis l'ERP",
          "category": "API publique",
          "isBillable": true
        },
        "relationships": [
          {
            "targetId": "{TICKET_ID}",
            "targetDataSet": "tickets",
            "targetItemType": "ticket",
            "relationshipType": "parent"
          }
        ]
      }
    }
  ]
}

Un élément update ou delete doit avoir son propre id et son propre ifMatch. Envoyez ifMatch comme une chaîne contenant l'ETag actuel :

{
  "items": [
    {
      "operation": "update",
      "id": "{WORKTIME_ID}",
      "ifMatch": "\"{CURRENT_ETAG}\"",
      "update": {
        "attributes": {
          "time": 2700
        }
      }
    },
    {
      "operation": "delete",
      "id": "{OTHER_WORKTIME_ID}",
      "ifMatch": "\"{OTHER_CURRENT_ETAG}\""
    }
  ]
}

Vérifiez chaque élément avec index, operation et status. Un résultat partiel peut utiliser HTTP 207 Multi-Status ; l'échec d'un élément ne confirme pas le succès des autres.


Heures de travail - supprimer un enregistrement

La suppression est irréversible du point de vue de l'API publique. Avant l'opération, relisez l'ETag et vérifiez que l'UUID et le customId désignent le bon enregistrement :

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"

Le résultat attendu est HTTP 200 OK avec data=true. Relisez ensuite l'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 lecture suivante doit renvoyer HTTP 404 Not Found avec workTime_not_found. Vérifiez également une liste filtrée par customId pour confirmer que l'enregistrement ne réapparaît pas.


Heures de travail - sans fichiers ni épingle

Les heures de travail ne disposent ni d'un module de fichiers ni d'une action d'épingle. N'utilisez pas ces routes :

/api/v1/worktimes/{WORKTIME_ID}/files
/api/v1/worktimes/{WORKTIME_ID}/pin

Traiter une heure de travail comme un objet possédant des fichiers ou une épingle n'est pas prévu par le contrat. Si une durée doit être accompagnée d'un document, enregistrez le fichier sur un objet qui accepte les fichiers, par exemple un ticket ou un document, et conservez la liaison dans le système d'intégration.


Heures de travail - erreurs et limites

HTTP
Code
Signification et réaction
400
validation_failed
Champ, parent ou Work Task déjà occupé non valide. Corrigez les données.
401
authentication_required
Vérifiez les deux en-têtes de clé et l'adresse de l'installation.
403
public_api_scope_denied
Ajoutez le scope requis à la clé dans Paramètres -> API.
403
workTime_parent_access_denied
Utilisez un parent accessible au caller.
404
workTime_not_found
Vérifiez l'UUID, l'adresse et l'accès à l'enregistrement.
409
idempotency_conflict
Cette clé d'idempotence a été utilisée avec un autre body.
412
if_match_failed
Relisez l'enregistrement et utilisez un nouvel ETag.
428
if_match_required
Ajoutez la valeur If-Match actuelle.
428
idempotency_key_required
Ajoutez un Idempotency-Key unique à la mutation.
429
rate_limit_exceeded
Appliquez un backoff et respectez les en-têtes de limite.
503
tenant_context_unavailable
Relancez avec un backoff sans modifier le body.

Après une erreur, enregistrez le statut HTTP, code et meta.requestId, mais jamais le secret de la clé. Pour HTTP 400, consultez aussi errors, qui indique le champ ou l'élément de tableau concerné.

Lisez les limites de requêtes et de batch dans data.capabilities.limits. Les en-têtes X-RateLimit-Limit et X-RateLimit-Remaining permettent d'adapter le rythme de synchronisation.


Heures de travail - synchronisation et n8n

Pour synchroniser un ERP, un helpdesk ou n8n, utilisez un customId attribué par le système externe, par exemple ERP-WORKTIME-{external-id}. N'utilisez pas le titre comme clé de déduplication, car deux sessions peuvent avoir la même description.

Dans n8n, un nœud HTTP Request suffit. Enregistrez X-Codenica-Client-Id, X-Codenica-Client-Secret et Accept: application/json dans les identifiants d'en-têtes. Ajoutez un Idempotency-Key unique aux requêtes POST.

{
  "itemType": "worktime",
  "attributes": {
    "customId": "N8N-WORKTIME-{{$execution.id}}",
    "date": "2026-09-06T09:00:00Z",
    "time": 1800,
    "title": "Temps synchronisé par n8n",
    "category": "API publique",
    "isBillable": true
  },
  "relationships": [
    {
      "targetId": "{{$json.ticketId}}",
      "targetDataSet": "tickets",
      "targetItemType": "ticket",
      "relationshipType": "parent"
    }
  ]
}

Pour une mise à jour, le workflow doit d'abord lire l'enregistrement, conserver data.meta.etag puis envoyer le PATCH avec cet ETag. Avec HTTP 412, relisez et traitez le conflit. Avec HTTP 429, utilisez un backoff limité. Le secret ne doit pas apparaître dans un nœud Code, les données d'entrée ni l'historique d'exécution.


Heures de travail - ordre recommandé des opérations

Une séquence d'intégration sûre peut suivre cet ordre :

1. Définir BASE_URL pour Cloud ou On-Premise.
2. Créer une clé dans Paramètres -> API et enregistrer le secret.
3. Lire GET /api/v1/context et vérifier la base de données, les scopes et les limites.
4. Lire GET /api/v1/worktimes/schema.
5. Sélectionner un Ticket, Change, Problem ou Release accessible.
6. Sélectionner éventuellement un Work Task libre et un Agent actif.
7. Créer l'enregistrement avec un parent et un nouvel Idempotency-Key.
8. Conserver l'UUID et l'ETag de la réponse.
9. Lire l'enregistrement avec include=relationships,users.
10. Utiliser un If-Match actuel et un nouvel Idempotency-Key pour les modifications.
11. Modifier les relations par l'endpoint approprié, jamais par les champs techniques.
12. Pour un import en masse, vérifier chaque élément du batch.
13. Lire un ETag actuel avant DELETE.
14. Confirmer HTTP 404 et l'absence du customId après DELETE.

Les règles essentielles sont simples : une heure de travail possède un parent principal, la durée est exprimée en secondes, la Work Task est facultative et ne peut être affectée qu'une fois, l'Agent est une relation utilisateur et les fichiers et épingles ne sont pas pris en charge. L'idempotence protège l'écriture et l'ETag protège la version de l'enregistrement.