Créer un devis, une facture ou une commande
Ce guide montre comment construire un devis, une facture, une commande client ou une commande fournisseur avec des lignes correctement tarifées, savoir ce qui l'emporte lorsqu'une grille tarifaire et un unit_price explicite se contredisent, et mettre à jour les lignes d'un document sans perdre des données que vous ne vouliez pas toucher. Il vous faut seulement un jeton d'API et votre URL de base, comme indiqué dans le guide de démarrage ; pour le prix propre d'un produit ou d'un service et la façon de le lier à une grille tarifaire, consultez Créer et utiliser une grille tarifaire, et pour l'enveloppe d'écriture, PATCH par rapport à PUT, et le mécanisme des erreurs de validation en général, consultez Créer, modifier et supprimer des fiches — ce guide ne couvre que ce qui est spécifique aux lignes et aux totaux de document.
1. Le plus petit document qui fonctionne
POST /api/v1/quotes — les trois autres fonctionnent de la même façon pour les lignes, mais chacun exige en plus quelques champs d'en-tête qui lui sont propres — avec une seule ligne :
curl -sS -X POST "https://app.initiative-crm.com/api/v1/quotes" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"subject":"Minimal quote","account_id":"61","assigned_user_id":"7206","line_items":[{"product_id":"68","quantity":1}]}}}' \
| jq '.data.attributes.line_items[0]'{
"id": "e1017fa9-afa7-4e83-ac8c-347c77c208f1",
"product_id": { "id": "68", "label": "Initiative License" },
"quantity": 1,
"unit_price": 99,
"discount_percent": 0,
"discount_amount": 0,
"discount": 0,
"tax_percent": 0,
"tax_amount": 0,
"line_subtotal": 99,
"line_total": 99,
"qtyinstock": 0,
"comment": ""
}unit_price (99) provient du prix propre du produit — rien d'autre n'a été envoyé. Le id de la ligne est un UUID généré par le serveur, pas un entier séquentiel ; vous ne le définissez jamais.
2. Tout ce qu'une ligne peut porter
| Champ | Type | Remarques |
|---|---|---|
product_id / service_id | référence, chaîne d'identifiant simple | Exactement l'un des deux doit être défini. Les deux ou aucun sont rejetés — voir Erreurs courantes. |
quantity | nombre | Doit être > 0. |
unit_price | nombre | Le prix de la ligne. Ignoré lorsque pricebook_id est défini sur la même ligne — voir le § 3. Omettez les deux et le prix propre de l'article catalogue est utilisé (§ 1). |
pricebook_id | référence, chaîne d'identifiant simple, écriture seule | Tarife la ligne à partir de la grille tarifaire indiquée plutôt qu'à partir du prix propre de l'article. |
discount_percent / discount_amount | nombre | Se combinent de façon additive dans le discount calculé (§ 4). |
comment | chaîne | Librement modifiable ; aucun comportement calculé. |
product_id, service_id et pricebook_id s'écrivent et se lisent comme de simples chaînes d'identifiant ("68"), la même convention que n'importe quel autre champ référence — voir Champs, formats et mise en forme des réponses. Tout le reste que renvoie une ligne (id, discount, tax_percent, tax_amount, line_subtotal, line_total, qtyinstock) est calculé côté serveur — voir le § 4.
3. Tarifer une ligne à partir d'une grille tarifaire
Définissez pricebook_id à la place de (ou en plus de) unit_price :
curl -sS -X POST "https://app.initiative-crm.com/api/v1/quotes" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"subject":"Full quote","account_id":"61","line_items":[{"product_id":"68","quantity":10,"pricebook_id":"63","comment":"Priced from Wholesale 2026"}]}}}' \
| jq '.data.attributes.line_items[0]'{
"id": "c8148d72-a51d-4637-b68c-b9a0e41083ec",
"product_id": { "id": "68", "label": "Initiative License" },
"quantity": 10,
"unit_price": 79,
"discount_percent": 0,
"discount_amount": 0,
"discount": 0,
"tax_percent": 0,
"tax_amount": 0,
"line_subtotal": 790,
"line_total": 790,
"qtyinstock": 0,
"comment": "Priced from Wholesale 2026"
}unit_price est revenu à 79 — le prix convenu de la grille pour ce produit, pas le 99 propre au produit. Voici maintenant le piège principal : que se passe-t-il si vous envoyez à la fois pricebook_id et un unit_price explicite sur la même ligne ?
curl -sS -X POST "https://app.initiative-crm.com/api/v1/quotes" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"subject":"Precedence","account_id":"61","line_items":[{"product_id":"68","pricebook_id":"63","unit_price":1.00,"quantity":1}]}}}' \
| jq '.data.attributes.line_items[0].unit_price'79Pas 1. Si vous envoyez les deux, la grille l'emporte et votre unit_price est ignoré silencieusement — aucune erreur, aucun avertissement, juste le prix de la grille dans la réponse. N'envoyez unit_price que lorsque vous voulez délibérément écraser à la fois le prix catalogue et toute grille tarifaire.
4. Ce que nous calculons pour vous
Ne les envoyez jamais — ils sont en lecture seule, calculés côté serveur à partir des données du § 2 :
tax_percent/tax_amount— dérivés de la taxe par défaut du produit ou du service. Les deux valent 0 dans ces exemples car les articles catalogue ne portent aucune taxe par défaut.discount—discount_percentetdiscount_amountcombinés de façon additive. Une ligne avecquantity: 2,unit_price: 95,discount_percent: 10est revenue avecdiscount: 19(10 % de190) ; une ligne avec seulementdiscount_amount: 100défini est revenue avecdiscount: 100.line_subtotal— valeur de la ligne après remise, avant taxe. La ligne95/10 %ci-dessus :2 × 95 = 190, moins19de remise =171.line_total— valeur de la ligne après remise, taxe incluse. Identique àline_subtotalici puisque la taxe était0.qtyinstock— stock disponible pour le produit référencé au moment où la fiche a été lue (les services renvoient toujours0).
Le bloc group.totals, au niveau du document, est calculé de la même façon, un niveau au-dessus — voir le § 5.
5. Chiffres au niveau du document
Une création peut aussi définir un bloc group pour les frais de port, une remise globale et un ajustement forfaitaire, le tout intégré dans group.totals :
curl -sS -X POST "https://app.initiative-crm.com/api/v1/quotes" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"subject":"Full quote","account_id":"61","line_items":[
{"product_id":"68","quantity":10,"pricebook_id":"63"},
{"product_id":"68","quantity":2,"unit_price":95.00,"discount_percent":10},
{"service_id":"69","quantity":3,"unit_price":750.00,"discount_amount":100}
],"group":{"shipping":{"amount":50},"global_discount":{"percent":5},"adjustment":-1.23}}}}' \
| jq '.data.attributes.group'{
"totals": { "subtotal": 2955.45, "tax_total": 0, "total": 3004.22 },
"shipping": { "cost": 0, "amount": 50, "percent": 0 },
"adjustment": -1.23,
"margin": 0,
"margin_percent": 0,
"global_discount": { "amount": 0, "percent": 5, "tax_amount": 0 }
}shipping— envoyé sous la forme{"amount":50}, relu avec deux sous-champs supplémentaires (cost,percent) non illustrés ici.global_discount— envoyé sous la forme{"percent":5}; appliqué à la somme desline_subtotalde toutes les lignes (790 + 171 + 2150 = 3111;3111 × 0.95 = 2955.45, ce qui correspond àtotals.subtotal).adjustment— un nombre forfaitaire, ajouté directement danstotals.total(2955.45 + 50 de frais de port − 1.23 d'ajustement = 3004.22).margin/margin_percent— calculés ; les deux valent 0 dans ces exemples car les articles catalogue n'ont aucune donnée de prix de revient permettant de calculer une marge.
group.totals lui-même est entièrement calculé — ne l'envoyez pas.
6. Mettre à jour un document
Avertissement : envoyer
line_itemssur unPATCHremplace l'ensemble complet. Il n'existe aucun moyen d'ajouter ou de supprimer une seule ligne sans renvoyer toutes les lignes que vous voulez conserver.
Un devis créé avec deux lignes (un produit et un service) :
# PATCH sans line_items — les deux lignes restent
curl -sS -X PATCH "https://app.initiative-crm.com/api/v1/quotes/1596" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"subject":"Replace test renamed"}}}' \
| jq '.data.attributes.line_items | length'2# PATCH avec une seule line_item — l'autre disparaît
curl -sS -X PATCH "https://app.initiative-crm.com/api/v1/quotes/1596" \
-H "Authorization: Bearer itv_YOUR_TOKEN_HERE" -H "Content-Type: application/json" \
-d '{"data":{"attributes":{"line_items":[{"product_id":"68","quantity":5}]}}}' \
| jq '(.data.attributes.line_items | length), .data.attributes.line_items[0].quantity'1
5Omettre line_items entièrement laisse les lignes existantes inchangées (2). Envoyer line_items — même avec une seule entrée — remplace l'ensemble complet (1, quantity: 5) ; la ligne qui n'a pas été renvoyée disparaît, elle n'est pas fusionnée. Pour l'enveloppe d'écriture PATCH/PUT en général et le mécanisme des erreurs de validation, consultez Créer, modifier et supprimer des fiches.
Erreurs courantes
line_itemsvide.422 VALIDATION_ERROR, champline_items:{"field":"line_items","code":"REQUIRED","message":"At least one line item is required."}quantityà0(ou négatif).422 VALIDATION_ERROR, champline_items.0.quantity:Ce contrôle ne se déclenche que sur la première ligne fautive — une charge utile avec deux lignes à quantité nulle ne signale toujours que{"field":"line_items.0.quantity","code":"INVALID_FORMAT","message":"quantity must be greater than 0."}line_items.0.quantity, pas les deux. Comparez avec le contrôle de la grille tarifaire ci-dessous.product_idetservice_idtous les deux définis, ou aucun des deux.422 VALIDATION_ERROR, les deux cas renvoient le même champ/message, surline_items.0.product_id:{"field":"line_items.0.product_id","code":"INVALID_FORMAT","message":"Exactly one of 'product_id' or 'service_id' must be present."}pricebook_idpointant vers une grille dont la fenêtre de dates ne couvre pas aujourd'hui (ou dont la quantité de la ligne est en dehors de la plage de la grille).422 VALIDATION_ERROR, codePRICEBOOK_NOT_APPLICABLEsurline_items.N.pricebook_id:Contrairement au contrôle de quantité, celui-ci collecte les violations sur toutes les lignes fautives de la requête — deux lignes référençant toutes deux la même grille inapplicable reviennent sous la forme de deux entrées{"field":"line_items.0.pricebook_id","code":"PRICEBOOK_NOT_APPLICABLE","message":"Price book 64 is not valid on 2026-08-03 (window: 2025-01-01 — 2025-12-31)"}{"field":"line_items.0.pricebook_id","code":"PRICEBOOK_NOT_APPLICABLE","message":"Quantity 99999 is outside the allowed range for price book 63 (min: 1, max: 500)"}detailsdistinctes (line_items.0.pricebook_idetline_items.1.pricebook_id), pas seulement la première.- Envoyer à la fois
pricebook_idetunit_price, en s'attendant à ce queunit_pricel'emporte. Ce n'est pas le cas — aucune erreur dans un cas comme dans l'autre, le prix de la grille est utilisé silencieusement. Voir le § 3.
Updated about 1 month ago
