Lier des fiches entre elles

Ce guide montre comment découvrir à quoi un module donné peut être lié, lister ce qui est déjà lié, créer et supprimer des liens, reconnaître les deux façons différentes dont se manifeste un refus « vous ne pouvez pas faire ça », et attacher correctement des notes (commentaires) à une fiche. Il vous faut seulement un jeton d'API et votre URL de base, comme indiqué dans le guide de démarrage ; pour les noms de champs, les types et l'enveloppe d'écriture, consultez Champs, formats et mise en forme des réponses et Créer, modifier et supprimer des fiches. Les grilles tarifaires utilisent ce même mécanisme de relation pour porter un prix par lien — consultez Créer et utiliser une grille tarifaire pour ce cas particulier.

1. Découvrir ce qui peut être lié

GET /api/v1/{module}/related-modules liste chaque module auquel un module donné peut se relier, si le lien est modifiable via cette API, et le schéma de toute donnée propre au lien :

curl -sS -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" \
  "https://app.initiative-crm.com/api/v1/accounts/related-modules" \
  | jq '.data[] | {module: .id, writable: .attributes.writable, relation_attributes: .attributes.relation_attributes}'

Le slug du module de chaque entrée se trouve dans .id. Écrivez les clés jq en entier comme ci-dessus ({module: .id, …}) — module est un mot réservé de jq, la forme abrégée ne compile donc pas.

{ "module": "contacts", "writable": false, "relation_attributes": [] }
{ "module": "potentials", "writable": false, "relation_attributes": [] }
{ "module": "quotes", "writable": false, "relation_attributes": [] }
{ "module": "sales-orders", "writable": false, "relation_attributes": [] }
{ "module": "invoices", "writable": false, "relation_attributes": [] }
{ "module": "tasks", "writable": false, "relation_attributes": [] }
{ "module": "documents", "writable": true, "relation_attributes": [] }
{ "module": "helpdesk", "writable": false, "relation_attributes": [] }
{ "module": "products", "writable": true, "relation_attributes": [] }
{ "module": "services", "writable": true, "relation_attributes": [] }
{ "module": "comments", "writable": false, "relation_attributes": [] }
{ "module": "events", "writable": false, "relation_attributes": [] }

writable: true signifie que vous pouvez créer et supprimer ce lien via POST/DELETE .../related/{module} (étapes 3 et 5). Un tableau relation_attributes vide signifie que le lien lui-même ne porte aucune donnée au-delà de « ces deux fiches sont connectées » — à comparer avec les grilles tarifaires, où le même champ liste listprice (étape 4). writable: false ne recouvre pas une seule et même situation — deux raisons réelles distinctes le produisent, et elles refusent les écritures différemment (étape 6).

2. Lister ce qui est déjà lié

GET .../related/{module} sur une fiche liste ce à quoi elle est liée, paginé exactement comme n'importe quel autre endpoint de liste. contacts apparaît dans la découverte accounts ci-dessus — voici la paire compte/contact que les fixtures de ce guide mettent en place :

curl -sS -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" \
  "https://app.initiative-crm.com/api/v1/accounts/61/related/contacts?per_page=5" | jq '.data[] | {id, type}'
{ "id": "62", "type": "contacts" }

meta porte la même forme total/page/per_page/pages utilisée par tous les endpoints de liste. Lire une relation fonctionne toujours, que la relation soit modifiable ou non — le contact du compte apparaît ici même si, comme l'explique l'étape suivante, vous ne pouvez pas gérer ce lien précis avec POST/DELETE.

3. Créer un lien

La première chose évidente à essayer est de lier un contact à un compte avec POST :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/accounts/61/related/contacts" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"id":"62"}}' -w '\n%{http_code}\n'
{"error":{"code":"METHOD_NOT_ALLOWED","message":"Relation Accounts → Contacts is a reference-field reverse; set the owning entity attribute instead.","request_id":"..."}}
405

Le compte d'un contact n'est pas une jointure plusieurs-à-plusieurs — c'est le champ référence account_id propre au contact (le même champ que vous modifieriez avec PATCH, d'après Créer, modifier et supprimer des fiches). L'API refuse purement et simplement de gérer ce type de relation via l'endpoint générique de relation, et l'erreur vous indique quoi faire à la place. DELETE sur la même paire refuse de la même façon (étape 5).

Pour une relation réellement plusieurs-à-plusieurs, utilisez-en une qui est véritablement writable: true et sans donnée propre — contactsservices :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/contacts/62/related/services" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"id":"69"}}' -w '\n%{http_code}\n'
{"data":{"id":"69","type":"services","attributes":{"servicename":"Onboarding Day", "...": "..."}},"meta":{"request_id":"..."}}
201

201 pour un nouveau lien, comme pour n'importe quelle autre création. Répétez le même appel à l'identique :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/contacts/62/related/services" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"id":"69"}}' -o /dev/null -w '%{http_code}\n'
201

Toujours 201, pas 200. Seules les relations qui portent des données se comportent comme un upsert (c'est le cas des grilles tarifaires — voir l'étape 4 et Créer et utiliser une grille tarifaire). Une relation simple comme celle-ci n'a pas de ligne propre au lien à vérifier, donc chaque POST réussi renvoie 201, que la paire ait déjà été liée ou non — le lien sous-jacent n'existe toujours qu'une seule fois (GET .../related/services ne montre jamais de doublon), simplement le code de statut à lui seul ne peut pas vous dire si cet appel était le premier.

Une relation writable: true n'est pas une garantie que le lien persiste. accounts/contactsproducts signalent tous deux writable: true et un POST renvoie 201 avec les données du produit lié — mais un GET .../related/products qui suit revient vide, et l'endpoint inverse (GET /products/{id}/related/accounts) renvoie un 500 INTERNAL_ERROR. Vérifiez avec un GET de suivi la première fois que vous utilisez une relation que vous ne connaissez pas ; ne présumez pas du succès à partir du seul 201.

4. Liens qui portent des données

Certaines relations attachent leurs propres données au lien lui-même, en plus des champs que portent déjà les deux fiches liées — c'est ce que signifie un relation_attributes non vide à l'étape 1. Les grilles tarifaires sont l'exemple filé : lier un produit à une grille tarifaire porte un listprice qui appartient au lien, pas au produit :

curl -sS -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" "https://app.initiative-crm.com/api/v1/pricebooks/related-modules" \
  | jq '.data[] | {module: .id, writable: .attributes.writable, relation_attributes: .attributes.relation_attributes}'
{ "module": "products", "writable": true, "relation_attributes": [{ "field": "listprice", "type": "decimal", "mandatory": true }] }
{ "module": "services", "writable": true, "relation_attributes": [{ "field": "listprice", "type": "decimal", "mandatory": true }] }
{ "module": "comments", "writable": false, "relation_attributes": [] }

Parce que le lien lui-même porte des données, cette relation fait un véritable upsert : le premier POST pour une paire renvoie 201, et reposter la même paire renvoie 200 et met à jour listprice sur place, au lieu de toujours signaler 201 comme la relation simple de l'étape 3. Pour le parcours complet — créer une grille, lier des produits et des services à des prix convenus, modifier et supprimer ces prix, et résoudre quelle grille s'applique — consultez Créer et utiliser une grille tarifaire.

5. Supprimer un lien

DELETE .../related/{module}/{id} supprime un lien. En utilisant la même paire contacts/services de l'étape 3 :

curl -sS -X DELETE "https://app.initiative-crm.com/api/v1/contacts/62/related/services/69" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -o /dev/null -w '%{http_code}\n'
curl -sS -X DELETE "https://app.initiative-crm.com/api/v1/contacts/62/related/services/69" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -o /dev/null -w '%{http_code}\n'
204
204

Les deux appels renvoient 204, y compris le second, alors qu'il ne restait plus rien à supprimer — la suppression de lien est idempotente, et l'API ne transforme jamais « déjà disparu » en 404.

Les relations de type champ référence refusent DELETE de la même façon qu'elles refusent POST (étape 3) :

curl -sS -X DELETE "https://app.initiative-crm.com/api/v1/accounts/61/related/contacts/62" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -w '\n%{http_code}\n'
{"error":{"code":"METHOD_NOT_ALLOWED","message":"Relation Accounts → Contacts is a reference-field reverse; set the owning entity attribute instead.","request_id":"..."}}
405

Cet appel n'a strictement aucun effet de bord — c'est un refus pur et simple, pas un délien sans effet, et le lien compte/contact n'est pas affecté.

6. Relations en lecture seule

writable: false dans la découverte de l'étape 1 recouvre deux causes réelles distinctes, et elles refusent une écriture différemment. La première est le cas de l'inverse d'un champ référence déjà vu ci-dessus (accountscontacts, et de façon identique contactspotentials) : 405 METHOD_NOT_ALLOWED, « is a reference-field reverse; set the owning entity attribute instead ».

La seconde est une relation sans aucun mécanisme d'écriture ni de jointure sous-jacente — contactscomments et contactsevents sont tous deux véritablement en lecture seule :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/contacts/62/related/comments" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"id":"61"}}' -w '\n%{http_code}\n'
{"error":{"code":"NOT_FOUND","message":"No relation between Contacts and ModComments","request_id":"..."}}
404

404 NOT_FOUND, pas 405 — ces relations sont accessibles en lecture (les commentaires et les événements de calendrier d'un contact apparaissent normalement sur GET .../related/comments et GET .../related/events), mais il n'y a strictement aucune relation à écrire via cet endpoint. Les commentaires et les événements sont attachés à une fiche d'une autre façon — voir l'étape 7 pour les commentaires.

7. Attacher des notes à une fiche

Les notes sont leur propre module — nom interne ModComments, slug comments. Comme pour chaque module de cette API, le chemin du schéma prend le slug, pas le nom interne (GET /api/v1/modules/ModComments renvoie un 404 ; GET /api/v1/modules/comments est le bon). Son champ est commentcontent.

Créez une note en postant directement sur /comments, pas via un endpoint de relation. related_to est obligatoire et doit être un objet avec à la fois id et type :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/comments" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"attributes":{"commentcontent":"First note","related_to":{"id":"62","type":"contacts"}}}}'
{
  "data": {
    "id": "1600",
    "type": "comments",
    "attributes": {
      "commentcontent": "First note",
      "createdtime": "2026-08-03T15:49:12Z",
      "modifiedtime": "2026-08-03T15:49:12Z",
      "parent_comments": null,
      "assigned_user_id": { "id": "7206", "label": "Taylor Morgan" },
      "creator": { "id": "7206", "label": "Taylor Morgan" },
      "related_to": { "id": "62", "type": "contacts", "label": "Jean Dupont" }
    }
  }
}

related_to n'accepte qu'un ensemble fixe de modules cibles — leads, accounts, contacts, potentials, helpdesk — d'après la liste target_modules du schéma ; un devis, un produit ou une grille tarifaire ne peut pas se voir attacher une note de cette façon.

Une note peut être mise en fil sous une autre note, avec parent_comments, à condition que le parent soit sur la même fiche :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/comments" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"attributes":{"commentcontent":"A reply","related_to":{"id":"62","type":"contacts"},"parent_comments":"1600"}}}' \
  | jq '.data.attributes'
{
  "commentcontent": "A reply",
  "related_to": { "id": "62", "type": "contacts", "label": "Jean Dupont" },
  "parent_comments": { "id": "1600", "label": "First note" }
}

Omettre type sur related_to est rejeté :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/comments" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"attributes":{"commentcontent":"Bad","related_to":{"id":"62"}}}}'
{"error":{"code":"VALIDATION_ERROR","message":"Validation failed.","request_id":"...","details":[{"field":"related_to","code":"INVALID_FORMAT","message":"related_to must be an object with \"id\" and \"type\" keys."}]}}

422, code INVALID_FORMAT sur le champ related_to. Pointer parent_comments vers une note sur une fiche différente est également rejeté, avec un code dédié :

curl -sS -X POST "https://app.initiative-crm.com/api/v1/comments" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"attributes":{"commentcontent":"Wrong parent","related_to":{"id":"61","type":"accounts"},"parent_comments":"1600"}}}'
{"error":{"code":"VALIDATION_ERROR","message":"Validation failed.","request_id":"...","details":[{"field":"parent_comments","code":"INVALID_PARENT","message":"parent_comments must reference a comment on the same record."}]}}

422, code INVALID_PARENT — la note 1600 est attachée au contact 62, donc une nouvelle note attachée au compte 61 ne peut pas l'utiliser comme parent, même si la note elle-même existe.

Une fois écrite, une note ne peut plus être modifiée :

curl -sS -X PATCH "https://app.initiative-crm.com/api/v1/comments/1600" \
  -H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
  -d '{"data":{"attributes":{"commentcontent":"Edited"}}}' -w '\n%{http_code}\n'
{"error":{"code":"METHOD_NOT_ALLOWED","message":"Module comments does not support PATCH.","request_id":"..."}}
405

405 METHOD_NOT_ALLOWED — il n'existe strictement aucun chemin de mise à jour pour une note. Si une note est erronée, la seule option est de la laisser telle quelle et d'en ajouter une nouvelle ; il n'y a pas d'historique de modification à corriger.

Erreurs courantes

  • Envoyer related_to comme un simple identifiant au lieu de {"id": "…", "type": "…"}. 422 VALIDATION_ERROR, code INVALID_FORMAT sur le champ related_to (étape 7) — les deux clés sont requises, à chaque fois.
  • Une note parente sur une fiche différente. 422 VALIDATION_ERROR, code INVALID_PARENT : « parent_comments must reference a comment on the same record » (étape 7).
  • Essayer de modifier une note. 405 METHOD_NOT_ALLOWED — les notes n'ont aucun chemin de mise à jour une fois créées (étape 7).
  • Considérer un 201 répété sur une relation simple comme un bug. Seules les relations porteuses de données (grilles tarifaires) font un upsert vers 200 sur un lien répété ; une relation simple signale 201 à chaque fois qu'elle réussit, que la paire ait déjà été liée ou non (étape 3).
  • Supposer que writable: true signifie que le lien va réellement persister. C'est généralement le cas, mais vérifiez avec un GET de suivi la première fois que vous utilisez une relation que vous ne connaissez pas — products sous accounts comme sous contacts est une exception connue (étape 3).
  • S'attendre à un 404 pour un délien qui n'avait rien à supprimer. DELETE est idempotent et renvoie 204 dans les deux cas (étape 5).
  • Essayer de lier ou délier une relation de type champ référence (par ex. accountscontacts) via l'endpoint de relation. 405 METHOD_NOT_ALLOWED — définissez plutôt directement le champ référence sur la fiche enfant (étapes 3 et 5).

Did this page help you?