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

ChampTypeRemarques
product_id / service_idréférence, chaîne d'identifiant simpleExactement l'un des deux doit être défini. Les deux ou aucun sont rejetés — voir Erreurs courantes.
quantitynombreDoit être > 0.
unit_pricenombreLe 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_idréférence, chaîne d'identifiant simple, écriture seuleTarife la ligne à partir de la grille tarifaire indiquée plutôt qu'à partir du prix propre de l'article.
discount_percent / discount_amountnombreSe combinent de façon additive dans le discount calculé (§ 4).
commentchaîneLibrement 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'
79

Pas 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.
  • discountdiscount_percent et discount_amount combinés de façon additive. Une ligne avec quantity: 2, unit_price: 95, discount_percent: 10 est revenue avec discount: 19 (10 % de 190) ; une ligne avec seulement discount_amount: 100 défini est revenue avec discount: 100.
  • line_subtotal — valeur de la ligne après remise, avant taxe. La ligne 95/10 % ci-dessus : 2 × 95 = 190, moins 19 de remise = 171.
  • line_total — valeur de la ligne après remise, taxe incluse. Identique à line_subtotal ici puisque la taxe était 0.
  • qtyinstock — stock disponible pour le produit référencé au moment où la fiche a été lue (les services renvoient toujours 0).

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 des line_subtotal de toutes les lignes (790 + 171 + 2150 = 3111 ; 3111 × 0.95 = 2955.45, ce qui correspond à totals.subtotal).
  • adjustment — un nombre forfaitaire, ajouté directement dans totals.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_items sur un PATCH remplace 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
5

Omettre 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_items vide. 422 VALIDATION_ERROR, champ line_items :
    {"field":"line_items","code":"REQUIRED","message":"At least one line item is required."}
  • quantity à 0 (ou négatif). 422 VALIDATION_ERROR, champ line_items.0.quantity :
    {"field":"line_items.0.quantity","code":"INVALID_FORMAT","message":"quantity must be greater than 0."}
    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 line_items.0.quantity, pas les deux. Comparez avec le contrôle de la grille tarifaire ci-dessous.
  • product_id et service_id tous les deux définis, ou aucun des deux. 422 VALIDATION_ERROR, les deux cas renvoient le même champ/message, sur line_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_id pointant 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, code PRICEBOOK_NOT_APPLICABLE sur line_items.N.pricebook_id :
    {"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)"}
    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 details distinctes (line_items.0.pricebook_id et line_items.1.pricebook_id), pas seulement la première.
  • Envoyer à la fois pricebook_id et unit_price, en s'attendant à ce que unit_price l'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.

Did this page help you?