Créer, composer et envoyer un devis
Créer un devis depuis une affaire ou un contact, y ajouter les articles du catalogue, l'envoyer par un parcours d'acceptation ou par email, puis suivre son issue.
Un devis est une carte d'un pipeline chiffré : il circule dans ses étapes — brouillon, envoyé, accepté, refusé… —, et le
serveur calcule seul ses montants, sa TVA et ses totaux. Ce qui vaut pour une carte vaut pour lui — étapes, responsable, messages :
voir Faire avancer une carte. Un devis introuvable, ou que vous ne voyez pas, répond 404 à chacune
des routes ci-dessous, avant tout autre refus ; un devis que vous voyez sans pouvoir le modifier répond 403 à celles qui le
modifient.
1Créer le devis
POST https://staging-api.docage.com/PipelineItems/Quotes
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{
"SourceTypeName": "PipelineItem",
"SourceId": "00000000-0000-4000-8000-000000000401"
}curl -X POST "https://staging-api.docage.com/PipelineItems/Quotes" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"SourceTypeName": "PipelineItem",
"SourceId": "00000000-0000-4000-8000-000000000401"
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/PipelineItems/Quotes");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"SourceTypeName": "PipelineItem",
"SourceId": "00000000-0000-4000-8000-000000000401"
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/Quotes", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"SourceTypeName": "PipelineItem",
"SourceId": "00000000-0000-4000-8000-000000000401"
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;La réponse est l'identifiant du devis, en brouillon.
SourceTypeNameetSourceId: ce que le devis chiffre — une carte (PipelineItem: une affaire, un ticket), un contact (Contact) ou une société (Company). Sans eux, un devis seul.PipelineId: le pipeline chiffré, quand l'organisation en a plusieurs (GET https://staging-api.docage.com/PipelineItems/PricedPipelines) ; omis, le premier, hors celui de la carte d'origine.TemplateId: un modèle de devis du même pipeline (GET https://staging-api.docage.com/PipelineItems/QuoteTemplates?pipelineId=…), dont il reprend les lignes, la remise et les champs.
Le devis reprend l'interlocuteur et la société de sa source, et reçoit un régime de TVA proposé d'après les pays. Créé depuis une
carte, il lui est lié quand une relation unit leurs deux types — c'est le cas d'un devis et d'une affaire dans le pipeline livré —,
et, sans modèle, recopie ses lignes si elle est chiffrée. Vous en êtes le responsable ; sa validité est celle que règle le pipeline.
Une source introuvable, ou que vous ne voyez pas, répond 404 ; un refus répond 409 : aucun pipeline chiffré, source à la
corbeille, modèle absent de ce pipeline.
2Compléter la fiche
Les champs d'un devis sont ceux de son type ; dans le pipeline « Devis » livré, Objet (le titre), Card_DueDate (« Valable
jusqu'au »), Introduction et Conditions. Ils s'écrivent par leur nom, comme des propriétés :
PUT https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{
"Objet": "Rénovation du hall",
"Card_DueDate": "2026-12-15",
"MainContactId": "00000000-0000-4000-8000-000000000201"
}curl -X PUT "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"Objet": "Rénovation du hall",
"Card_DueDate": "2026-12-15",
"MainContactId": "00000000-0000-4000-8000-000000000201"
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Put, "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"Objet": "Rénovation du hall",
"Card_DueDate": "2026-12-15",
"MainContactId": "00000000-0000-4000-8000-000000000201"
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"Objet": "Rénovation du hall",
"Card_DueDate": "2026-12-15",
"MainContactId": "00000000-0000-4000-8000-000000000201"
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Créé depuis une société, un devis n'a pas d'interlocuteur : désignez-le (MainContactId), l'envoi l'exige. Les noms des champs se
lisent dans le type de carte : GET https://staging-api.docage.com/CustomEntityDefinitions/{DefinitionId} (Fields).
3Ajouter des articles
Le catalogue du pipeline :
GET https://staging-api.docage.com/Pipelines/00000000-0000-4000-8000-000000000302/Articles
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…curl "https://staging-api.docage.com/Pipelines/00000000-0000-4000-8000-000000000302/Articles" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…"using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Get, "https://staging-api.docage.com/Pipelines/00000000-0000-4000-8000-000000000302/Articles");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/Pipelines/00000000-0000-4000-8000-000000000302/Articles", {
headers: {
Authorization: `Bearer ${token}`
}
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Chaque article a son Id, sa référence (Code), sa désignation (Name), son unité, son prix unitaire HT (UnitPrice). Puis :
POST https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines/FromArticle
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{ "ArticleId": "00000000-0000-4000-8000-000000000851", "Quantity": 3 }curl -X POST "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines/FromArticle" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"ArticleId": "00000000-0000-4000-8000-000000000851",
"Quantity": 3
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines/FromArticle");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"ArticleId": "00000000-0000-4000-8000-000000000851",
"Quantity": 3
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines/FromArticle", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"ArticleId": "00000000-0000-4000-8000-000000000851",
"Quantity": 3
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;La ligne est une photographie de l'article : désignation, prix, prix de revient, TVA et périodicité y sont recopiés, et un
changement de tarif ne touchera pas ce devis. Position (le rang, à partir de 0) place la ligne ; ReplaceLineId remplace une ligne
existante, à sa place. La réponse est le devis recalculé : Lines, Totals (AmountExclTax, TaxAmount, AmountInclTax…) et
CanEdit. Un refus répond 409 avec son motif : article hors du catalogue du pipeline, devis sorti du brouillon.
4Retoucher les lignes
Tout le reste — une ligne libre, une section, un texte, une option, une remise, le régime de TVA — passe par le remplacement de
toutes les lignes : relisez-les (GET https://staging-api.docage.com/PipelineItems/{id}/Lines), puis renvoyez la liste entière.
PUT https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{
"Lines": [
{ "Id": "00000000-0000-4000-8000-000000000861", "Kind": 0, "ArticleId": "00000000-0000-4000-8000-000000000851", "Label": "Installation sur site", "Quantity": 3, "Unit": "h", "UnitPrice": 80, "VatRate": 20 },
{ "Kind": 1, "Label": "Options" },
{ "Kind": 0, "Label": "Formation", "Quantity": 1, "UnitPrice": 400, "VatRate": 20, "DiscountRate": 10, "IsOptional": true, "IsSelected": false }
],
"GlobalDiscountRate": 5
}curl -X PUT "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"Lines": [{"Id": "00000000-0000-4000-8000-000000000861", "Kind": 0, "ArticleId": "00000000-0000-4000-8000-000000000851", "Label": "Installation sur site", "Quantity": 3, "Unit": "h", "UnitPrice": 80, "VatRate": 20}, {"Kind": 1, "Label": "Options"}, {"Kind": 0, "Label": "Formation", "Quantity": 1, "UnitPrice": 400, "VatRate": 20, "DiscountRate": 10, "IsOptional": true, "IsSelected": false}],
"GlobalDiscountRate": 5
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Put, "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"Lines": [
{
"Id": "00000000-0000-4000-8000-000000000861",
"Kind": 0,
"ArticleId": "00000000-0000-4000-8000-000000000851",
"Label": "Installation sur site",
"Quantity": 3,
"Unit": "h",
"UnitPrice": 80,
"VatRate": 20
},
{
"Kind": 1,
"Label": "Options"
},
{
"Kind": 0,
"Label": "Formation",
"Quantity": 1,
"UnitPrice": 400,
"VatRate": 20,
"DiscountRate": 10,
"IsOptional": true,
"IsSelected": false
}
],
"GlobalDiscountRate": 5
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Lines", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"Lines": [{"Id": "00000000-0000-4000-8000-000000000861", "Kind": 0, "ArticleId": "00000000-0000-4000-8000-000000000851", "Label": "Installation sur site", "Quantity": 3, "Unit": "h", "UnitPrice": 80, "VatRate": 20}, {"Kind": 1, "Label": "Options"}, {"Kind": 0, "Label": "Formation", "Quantity": 1, "UnitPrice": 400, "VatRate": 20, "DiscountRate": 10, "IsOptional": true, "IsSelected": false}],
"GlobalDiscountRate": 5
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Kind:0une ligne chiffrée (sa désignationLabelest exigée),1une section (le sous-total qui la suit repart de zéro),2un texte,3un sous-total.- Une ligne relue garde son
Idet revient entière : une ligne absente est supprimée, une propriété omise est remise à zéro. GlobalDiscountRateabsent retire la remise globale ;VatSchemeabsent garde le régime (0TVA normale,1autoliquidation,2exonéré,3hors champ).- Les montants envoyés sont ignorés : le serveur les recalcule.
Un refus répond 409 avec son motif : devis sorti du brouillon, ligne chiffrée sans désignation, plus de 1000 lignes.
5Relire le PDF
GET https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Pdf
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…curl "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Pdf" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…"using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Get, "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Pdf");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Pdf", {
headers: {
Authorization: `Bearer ${token}`
}
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Le PDF est composé à la demande, dans la langue du devis. Un brouillon s'y lit en aperçu, sans numéro.
6Envoyer
Le parcours d'acceptation dit ce que fait l'envoi : faire valider le devis, le faire signer, ou le faire signer et encaisser un
acompte. GET https://staging-api.docage.com/PipelineItems/QuoteTransactionTemplates les liste : d'abord ceux de la plateforme — « Acceptation de devis »,
« Signature de devis », « Signature et acompte de devis » (IsSystem) —, puis ceux de l'organisation qui portent un seul participant
dynamique.
POST https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Send
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{ "TransactionTemplateId": "00000000-0000-4000-8000-000000000871" }curl -X POST "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Send" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"TransactionTemplateId": "00000000-0000-4000-8000-000000000871"
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Send");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"TransactionTemplateId": "00000000-0000-4000-8000-000000000871"
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/PipelineItems/00000000-0000-4000-8000-000000000801/Send", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"TransactionTemplateId": "00000000-0000-4000-8000-000000000871"
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Le devis reçoit son numéro, son PDF est composé — avec les conditions générales quand le pipeline les demande —, et le parcours
part aussitôt vers l'interlocuteur. La réponse est l'identifiant du parcours. Sans TransactionTemplateId, le parcours réglé sur le
pipeline, sinon « Acceptation de devis ».
Sans parcours, le devis part par email à l'interlocuteur, PDF joint, et l'acceptation se déclare ensuite à la main (§ 7) :
{ "TransactionTemplateId": "00000000-0000-0000-0000-000000000000", "Message": "<p>Bonjour, voici notre proposition.</p>" }La réponse est alors l'identifiant de l'email. Message est facultatif : sans lui, un texte par défaut, dans la langue du
destinataire.
L'envoi exige une ligne chiffrée, un interlocuteur, une validité non dépassée et un devis non clos ; sinon 409, avant tout effet,
comme pour un parcours à acompte qui ne pourrait pas être réglé (pas de taux d'acompte, pas de compte de paiement vérifié, devise
différente). Un devis en brouillon rejoint la première étape ouverte de son pipeline. Envoyé par un parcours, il annule celui qui
serait encore en cours pour lui : un seul parcours d'acceptation à la fois.
Un parcours créé qui ne démarre pas répond aussi 409, et le message le désigne : « Le devis est numéroté et son parcours créé,
mais ce parcours n'a pas pu démarrer (identifiant du parcours) : motif ». Ce parcours reste en base, rattaché au devis, et le devis
est déjà numéroté et rangé avec les devis envoyés. Corrigez la cause, puis lancez-le : POST https://staging-api.docage.com/Transactions/LaunchTransaction/{id}
avec cet identifiant ; ou renvoyez le devis, ce qui l'annule et en crée un autre.
Un devis qui laisse le client choisir — une option (IsOptional), une quantité ouverte (CustomerCanChangeQuantity) — lui donne,
par un parcours, une première étape où il fait ses choix.
7Suivre l'issue
L'issue du parcours fait avancer le devis : dans le pipeline « Devis » livré, terminé mène à « Accepté », refusé à « Refusé », expiré à
« Expiré ». Relisez le devis par GET https://staging-api.docage.com/PipelineItems/{id} : Status, StageId, Number, Amount (le total HT),
AmountInclTax.
- Accepté hors de l'application (accord oral, bon de commande reçu) :
PUT https://staging-api.docage.com/PipelineItems/{id}/Accept, sur un devis envoyé (Status1ou2) ; il passe à l'étape aboutie. - Refusé : déplacez-le vers l'étape échouée, avec son motif — voir Faire avancer une carte.
- Réviser :
POST https://staging-api.docage.com/PipelineItems/{id}/Revise, sur sa dernière version envoyée, dans un pipeline qui le permet. La réponse est l'identifiant de la révision, un brouillon neuf numéroté « -R2 » ; l'ancienne version passe à l'étape des devis remplacés, et ses parcours en cours sont annulés. Un brouillon, un devis accepté ou une version déjà révisée répondent409.
Dans un pipeline qui fait expirer ses devis — c'est le cas du pipeline livré —, un devis est valable jusqu'à sa date de validité incluse, et expire le lendemain ; son parcours d'acceptation se termine à la même date.
Les pièges
- Un devis envoyé est figé : ses lignes ne se modifient plus (
409) ; on le révise. - N'écrivez pas
Amountsur un devis : le serveur le recalcule depuis ses lignes à chaque changement de lignes et à l'envoi. - La validité s'écrit dans
Card_DueDate, pas dansDueDate: la colonne suit le champ, qui l'emporte à chaque enregistrement. - Un
409peut arriver après un premier effet, et son message le dit, avec l'identifiant de ce qui a été créé entre parenthèses : « Le devis a été créé, mais n'a pas pu être complété (identifiant du devis) », « Le devis est numéroté et son parcours créé, mais ce parcours n'a pas pu démarrer (identifiant du parcours) ». Relisez-le avant de recommencer. - Un modèle de devis ne s'envoie pas.