Devis
Le devis couvre la phase commerciale qui précède la facture : votre système propose une prestation, le client accepte ou refuse, et l'acceptation se transforme en facture de vente sans ressaisie. L'API expose ce parcours de bout en bout - création, finalisation, décision du client, conversion - avec à chaque étape un état de cycle de vie explicite. Le devis n'est pas un document réglementé : Scribee le produit en PDF, sans transmission au PPF (Portail Public de Facturation) ni sur le réseau Peppol.
Ce que Scribee fait pour vous
- Numérotation : le brouillon porte un numéro provisoire préfixé
DRAFT-; le numéro définitif est attribué à la finalisation, à partir du format de numérotation configuré sur l'entreprise depuis l'interface Scribee, avec une séquence qui s'incrémente à chaque devis et ne se réinitialise jamais. Unquote_numberenvoyé dans la charge utile est ignoré. - Calcul des montants : vous fournissez les lignes (quantité, prix unitaire, taux de TVA, remises) ; Scribee calcule les totaux HT, TVA et TTC et les sous-totaux par taux.
- Moyens de paiement et mentions légales : ils sont repris de la configuration de l'entreprise émettrice (comptes bancaires, mentions de retard de paiement, d'indemnité de recouvrement et d'escompte, forme juridique et capital social). Ils ne se pilotent pas depuis la charge utile de l'API ; vous les lisez sur le devis créé avec
include=payment_means,item_notes. - PDF : le document PDF est généré à la création et régénéré à chaque modification du brouillon puis à la finalisation ; vous le récupérez sur
GET /api/v1/quotes/{id}/download. - Expiration automatique : une fois par jour, tout devis
sentdont la date d'expiration est dépassée passe enexpired, sans action de votre part. - Conversion en facture :
POST /api/v1/quotes/{id}/convertclone le devis - parties, lignes, remises, notes, sous-totaux de TVA, moyens de paiement - dans une facture de vente en brouillon et lie les deux documents.
Le cycle de vie
Sept états, sept libellés :
| Code API | Libellé |
|---|---|
draft | Brouillon |
sent | Envoyé |
accepted | Accepté |
rejected | Refusé |
expired | Expiré |
cancelled | Annulé |
converted | Converti |
Les transitions se déclenchent sur PATCH /api/v1/quotes/{id}/transition, avec un corps {"quote": {"event": "..."}} et le scope write. Les six événements - finalize, accept, reject, expire, cancel, convert - sont tous déclenchables par l'API, sans restriction : à la différence de la facture, dont certains statuts de cycle de vie sont réservés à la plateforme ou au destinataire (Le cycle de vie d'une facture), le devis est entièrement piloté par votre système.
Chaque réponse expose lifecycle_available_transitions, la liste des événements réellement déclenchables depuis l'état courant. Cette liste tient compte des préconditions, pas seulement de l'état : un brouillon sans expiration_date renvoie ["cancel"], sans finalize.
Un point de vocabulaire pour vos écrans : un devis rejected se libelle Refusé ; sur une facture, le statut 210 se libelle Refusée et le 213 Rejetée - trois notions distinctes, à ne pas confondre.
rejected, expired, cancelled et converted sont des états terminaux : aucune transition n'en sort. La suppression (DELETE /api/v1/quotes/{id}, scope write) n'est possible qu'en draft et cancelled ; les autres états conservent le document.
Lister les devis
GET /api/v1/workspaces/{workspace_id}/quotes renvoie les devis du workspace, scope read. La liste est paginée (page, per_page - 20 par défaut, 100 au maximum) et triable (sort_by : issue_date, expiration_date, quote_number, tax_inclusive_amount, total_amount_excluding_taxes, lifecycle_state, created_at ; sort_order : asc ou desc ; tri par défaut : issue_date décroissant). Une valeur de tri inconnue est ignorée et le tri par défaut s'applique.
Elle n'expose aucun filtre : ni lifecycle_state, ni plage de dates, ni entreprise, ni client, ni recherche. Ne prévoyez pas de filtrage côté requête, contrairement à la liste des factures ; filtrez après réception ou paginez l'ensemble.
curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/quotes?per_page=50&sort_by=created_at&sort_order=desc" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
La réponse porte data (le tableau de devis) et meta (current_page, per_page, total_pages, total_count). include accepte les mêmes valeurs qu'en lecture unitaire : lines, payment_means, tax_subtotals, item_notes ; une valeur inconnue est ignorée sans erreur.
Étape 1 : créer le brouillon
Cet appel crée un devis à l'état draft dans votre workspace. Rien ne part vers le client ni vers aucun réseau, et le brouillon se supprime à tout moment : l'opération est entièrement réversible. Le scope write est requis. company_id désigne l'entreprise émettrice, customer_id un client de cette entreprise (Clients et fournisseurs) dont les coordonnées sont copiées dans le devis.
Champs d'en-tête acceptés : company_id, customer_id, billing_address_id, issue_date, expiration_date, currency_code, global_discount_value, global_discount_type, global_discount_vat_rate, lines. issue_date omise prend la date du jour, currency_code omis prend EUR. Renseigné, currency_code doit figurer dans la liste ISO 4217 que Scribee embarque, à la casse exacte : EUR passe, eur et USd sont refusés en 422, comme l'est un jeton bien formé qui ne nomme aucune devise. Aucune validation ne rejette une expiration_date dans le passé : le devis part alors en expired au prochain passage du traitement d'expiration.
billing_address_id choisit laquelle des adresses de facturation du client est recopiée dans le bloc acheteur. Un identifiant inconnu, ou qui ne désigne pas une adresse de facturation de ce client, ne renvoie pas d'erreur : l'adresse de facturation par défaut est utilisée à la place. Relisez le bloc buyer de la réponse pour vérifier l'adresse retenue.
Les deux formes de ligne
Une ligne est soit catalogue, soit libre ; c'est la présence de product_id qui tranche.
- Ligne catalogue (
product_idrenseigné) : le nom, l'unité et le prix unitaire viennent du produit. Unarticle_nameou ununit_priceenvoyé sur cette ligne est ignoré sans avertissement. Seulvat_rateprime sur le taux du produit. Unproduct_idqui ne désigne pas un produit vendable de l'entreprise renvoie422(Produit introuvable.). - Ligne libre (
product_idabsent) :article_nameetunit_pricesont requis. L'unité est toujoursC62(unité) et le taux de TVA vaut 20 sivat_rateest absent.
Une ligne sans product_id et sans article_name est silencieusement écartée. Elle ne provoque aucune erreur : elle n'apparaît simplement pas dans le devis. Si toutes les lignes sont écartées, la création échoue en 422 (Au moins une ligne de devis est requise.).
Chaque ligne accepte aussi vat_rate, note, note_type (general_information par défaut), discount_value et discount_type. quantity est obligatoire sur toute ligne retenue, catalogue comme libre : c'est le seul champ numérique sans valeur par défaut, et son absence est refusée en 422.
Les remises
Une remise se pose au niveau du document (global_discount_value, global_discount_type, global_discount_vat_rate) ou de la ligne (discount_value, discount_type).
Seule la chaîne exacte percentage est lue comme un pourcentage. Toute autre valeur de global_discount_type ou de discount_type - fixed, percent, %, une chaîne vide, le champ absent - fait de la valeur un montant absolu dans la devise du document. Une remise envoyée avec "discount_type": "percent" et "discount_value": 10 retire donc 10 unités monétaires, pas 10 %.
Une remise dont la valeur vaut zéro est ignorée. global_discount_vat_rate est accepté par la charge utile mais n'est jamais lu : le taux de TVA d'une remise de document est dérivé des lignes qu'elle couvre, et la remise est répartie entre les catégories de TVA présentes au prorata de leur base.
:::note Les champs numériques sont contrôlés avant écriture
Une valeur non numérique dans quantity, unit_price (ligne libre), vat_rate, discount_value ou global_discount_value est refusée en 422, avec un message qui nomme le champ en cause. Une quantity absente est refusée de la même façon. Rien n'est créé ni modifié. Cela vaut pour POST comme pour PATCH.
Deux valeurs échappent au contrôle parce qu'elles ne sont jamais converties : l'unit_price d'une ligne catalogue, qui est ignoré au profit du prix du produit, et les champs d'une ligne écartée.
:::
curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/quotes \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"quote": {
"company_id": YOUR_COMPANY_ID,
"customer_id": YOUR_CUSTOMER_ID,
"issue_date": "2026-07-31",
"expiration_date": "2026-08-30",
"currency_code": "EUR",
"lines": [
{
"article_name": "Prestation de conseil",
"quantity": 2,
"unit_price": 500.0,
"vat_rate": 20.0
}
]
}
}'
Réponse 201, abrégée aux champs utiles ici. La réponse réelle porte en plus les blocs seller et buyer, les sept totaux monétaires, company_id, created_at et updated_at :
{
"data": {
"id": 314,
"quote_number": "DRAFT-5f2a9c3e0b1d4e77",
"issue_date": "2026-07-31",
"expiration_date": "2026-08-30",
"currency_code": "EUR",
"lifecycle_state": "draft",
"lifecycle_available_transitions": ["finalize", "cancel"],
"total_amount_excluding_taxes": 1000.0,
"tax_inclusive_amount": 1200.0,
"converted_invoice_id": null
}
}
Le brouillon se relit avec GET /api/v1/quotes/314?include=lines (valeurs d'include : lines, payment_means, tax_subtotals, item_notes) et se modifie avec PATCH /api/v1/quotes/314 tant qu'il est en draft ; chaque modification régénère le PDF.
:::caution PATCH remplace le devis, il ne le complète pas
Le corps du PATCH est traité comme un devis complet. Chaque appel détruit puis reconstruit les lignes, les remises, les frais, les ventilations de TVA, les moyens de paiement, les notes et les tiers : envoyez systématiquement company_id, customer_id et le tableau lines entier, même pour ne changer qu'un champ. Les attributs omis sont réinitialisés - issue_date prend la date du jour, expiration_date devient nulle et currency_code repasse à EUR. Un currency_code présent mais hors de la liste ISO 4217 fait échouer le remplacement entier en 422 : rien n'est détruit ni reconstruit, le devis reste tel qu'il était. Un corps partiel répond 422 ("Entreprise introuvable.").
:::
Étape 2 : finaliser
finalize fige le devis et lui attribue son numéro définitif. Cet appel ne transmet rien au client - l'envoi du PDF est un appel distinct (Envoyer par email) - mais il consomme un numéro dans la séquence de l'entreprise, qui ne revient pas en arrière, et le devis quitte définitivement l'état draft. Deux préconditions : une expiration_date renseignée, et un format de numérotation de devis configuré sur l'entreprise depuis l'interface Scribee.
:::warning Un finalize refusé faute d'expiration_date consomme quand même un numéro
Le numéro définitif est réservé avant la vérification de l'expiration_date. Un finalize sur un brouillon sans date d'expiration renvoie bien 422 et laisse le devis en draft, mais la séquence de l'entreprise a déjà été incrémentée : ce numéro est perdu et votre numérotation de devis présentera un trou. Chaque tentative supplémentaire en consomme un de plus.
Vérifiez lifecycle_available_transitions avant d'appeler : tant que finalize n'y figure pas, la précondition n'est pas remplie. Renseignez d'abord la date par un PATCH complet (voir l'encadré ci-dessus), puis finalisez.
:::
curl -X PATCH https://app.scribee.tech/api/v1/quotes/314/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"quote": {"event": "finalize"}}'
Réponse 200, abrégée : comme à la création, la réponse réelle porte l'ensemble des champs du devis.
{
"data": {
"id": 314,
"quote_number": "DEV-2026-07-42",
"lifecycle_state": "sent",
"lifecycle_available_transitions": ["accept", "reject", "expire", "cancel"]
}
}
Étape 3 : enregistrer la décision du client
Le client répond hors API - c'est votre système qui enregistre sa décision. Cet appel ne change que l'état du devis, rien ne part vers l'extérieur, et le PDF n'est pas régénéré. accept ouvre la conversion en facture ; reject, cancel et expire ferment le devis définitivement.
curl -X PATCH https://app.scribee.tech/api/v1/quotes/314/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"quote": {"event": "accept"}}'
Réponse 200, abrégée :
{
"data": {
"id": 314,
"lifecycle_state": "accepted",
"lifecycle_available_transitions": ["convert"]
}
}
Étape 4 : convertir en facture
La conversion crée une facture de vente à l'état brouillon dans le même workspace, copie du devis ligne à ligne, et fait passer le devis en converted - un état terminal : un devis ne se convertit qu'une fois. Rien n'est transmis : la facture démarre en brouillon, et le dépôt lui-même n'envoie rien vers le PPF ni sur le réseau Peppol (Émettre une facture). Seul un devis accepted se convertit. Le scope write est requis.
:::danger N'utilisez jamais l'événement convert du endpoint de transition
PATCH /api/v1/quotes/{id}/transition avec {"quote": {"event": "convert"}} fait passer le devis en converted sans créer aucune facture. converted est terminal : le devis n'a plus aucune transition disponible, et POST /api/v1/quotes/{id}/convert répond dès lors 403 de façon définitive, puisqu'il exige l'état accepted. Il n'existe aucun moyen de revenir en arrière ni de rattraper la facture. Passez toujours par POST /api/v1/quotes/{id}/convert.
:::
curl -X POST https://app.scribee.tech/api/v1/quotes/314/convert \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Réponse 201 : le devis, avec l'identifiant de la facture créée dans converted_invoice_id. La réponse est celle du devis, abrégée ici aux champs utiles.
{
"data": {
"id": 314,
"quote_number": "DEV-2026-07-42",
"lifecycle_state": "converted",
"lifecycle_available_transitions": [],
"converted_invoice_id": 9021
}
}
La facture créée n'est pas la copie conforme du devis sur trois points, à connaître si votre système rapproche les deux documents :
- son
issue_dateest la date du jour de la conversion, pas la date d'émission du devis ; - elle porte un numéro provisoire, comme toute facture en brouillon ; le numéro définitif est attribué au dépôt ;
- son
customization_idest positionné sur le profil Factur-X EXTENDED. Une facture que vous créez directement par l'API n'enregistre, elle, que lecustomization_idque vous envoyez ; sans valeur envoyée, le champ restenull, mais les fichiers produits portent le même profil EXTENDED-CTC-FR (Formats et téléchargements).
Une ventilation de TVA en catégorie E - la franchise en base de TVA (article 293 B du CGI), qu'un devis établi dans l'application Scribee peut déclarer - arrive sur la facture avec son motif d'exonération : dans les tax_subtotals de la facture, tax_exemption_reason_code vaut VATEX-FR-FRANCHISE et tax_exemption_reason en donne le libellé. Un devis créé par l'API ne déclare pas de catégorie : ses ventilations sont en S, ou en Z à taux zéro.
La suite du parcours - relecture du brouillon de facture, dépôt, statuts AIFE (Agence pour l'informatique financière de l'État) - se joue sur la facture 9021 : Émettre une facture.
Étape 5 : télécharger le PDF
Lecture sans effet de bord, disponible à tout état dès que le PDF est généré, avec le scope read. La réponse est une redirection 302 vers une URL de téléchargement Scribee valable 5 minutes au plus : elle cesse de fonctionner plus tôt si le PDF du devis est régénéré entre-temps (modification du brouillon, finalize). Suivez-la aussitôt avec -L, sans la conserver. Une fois expirée ou invalidée, elle répond 404 ; rappelez l'endpoint pour en obtenir une nouvelle. Le fichier est servi par Scribee, jamais par un lien direct vers un stockage tiers.
curl -L https://app.scribee.tech/api/v1/quotes/314/download \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-o devis-DEV-2026-07-42.pdf
Ce qui se passe ensuite
- Expiration automatique : une fois par jour, Scribee passe en
expiredtout devissentdont l'expiration_dateest dépassée. Si votre système suit les devis en cours, attendez-vous à voir cet état apparaître sans appel de votre part. - Génération du PDF : elle est asynchrone. Après une création ou une modification, le téléchargement peut répondre
404pendant quelques instants, le temps que le nouveau PDF soit produit - une modification purge l'ancien PDF avant de produire le nouveau, la fenêtre d'indisponibilité est réelle et pas seulement une latence de cache. La finalisation se comporte autrement : elle ne purge rien. La réponse à la transition expose déjà le numéro définitif alors que le téléchargement sert encore l'ancien PDF, celui qui porte le numéro de brouillon. Attendez que le PDF téléchargé porte le numéro renvoyé par la transition avant de l'envoyer au client. - Webhook de conversion : la facture créée par la conversion émet l'événement
invoice.createdvers vos endpoints abonnés (Webhooks), comme toute création de facture. Le devis, lui, n'émet aucun webhook : ni sa création, ni ses transitions, ni sa suppression. - Aucun échange réglementaire : ni le devis ni sa conversion ne déclenchent de transmission ; le circuit AIFE commence au dépôt de la facture issue de la conversion.
Erreurs et cas limites
422 : valeur numérique invalide
Les champs listés dans l'encadré de l'étape 1 sont contrôlés avant toute écriture, sur la création comme sur la modification. Une valeur non numérique renvoie un 422 dont le details.base nomme le champ : La quantité doit être un nombre., Le prix unitaire doit être un nombre., Le taux de TVA doit être un nombre., La remise de ligne doit être un nombre. ou La remise globale doit être un nombre.. Une quantity absente renvoie La quantité est requise pour les lignes de devis.. Le devis n'est ni créé ni modifié.
Le contrôle passe après ceux du produit et du prix unitaire libre : une charge utile qui cumule les deux défauts renvoie d'abord Produit introuvable. ou Le prix unitaire est requis pour les lignes libres..
400 : enveloppe quote absente ou page hors limites
Deux cas, tous deux dans l'enveloppe d'erreur habituelle. Un corps sans l'enveloppe quote de premier niveau est rejeté en 400 avant d'atteindre la logique du devis, avec error à bad_request et le message "Le corps de la requête est manquant ou mal formé". Et sur la liste, un numéro de page au-delà de la dernière renvoie 400 avec la même clé bad_request :
{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}
422 : transition impossible depuis l'état courant
Déclencher un événement non permis - par exemple accept sur un brouillon - renvoie :
{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Impossible de accept un devis à l'état draft : Event 'accept' cannot transition from 'draft'.",
"details": {}
}
Lisez lifecycle_available_transitions avant de déclencher un événement, et basez vos traitements sur le statut et la clé error, pas sur le texte de message (Conventions de l'API). Un finalize sans expiration_date renseignée échoue de la même façon - mais il a déjà consommé un numéro de séquence, voir l'avertissement de l'étape 2. Un nom d'événement inconnu renvoie le même statut avec le message Événement de transition invalide : approve..
422 : pas de format de numérotation
finalize échoue tant que l'entreprise n'a pas de format de numérotation de devis :
{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Impossible de finaliser : l'entreprise n'a pas de format de numérotation de devis configuré.",
"details": {}
}
Le format se configure dans les paramètres de l'entreprise, depuis l'interface Scribee. Une fois le format en place, rejouez le finalize : le devis n'a pas changé d'état, et aucun numéro de séquence n'a été consommé dans ce cas précis.
422 : validation de la charge utile
Vaut pour la création comme pour la modification. La charge utile exige une entreprise et un client existants, au moins une ligne retenue, et - si vous le renseignez - un currency_code qui figure dans la liste ISO 4217. Le corps porte "message": "La validation a échoué" et le détail dans details.base :
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Au moins une ligne de devis est requise."]
}
}
Les autres valeurs de details.base sur ce parcours : Entreprise introuvable. (company_id absent, inconnu, ou entreprise non habilitée à la facturation de vente), Client introuvable. (customer_id absent ou étranger à cette entreprise), Produit introuvable. (product_id d'une ligne catalogue), Le prix unitaire est requis pour les lignes libres. et Devise ne fait pas partie de la liste des devises ISO 4217 (currency_code absent de la liste, ou dans une autre casse que les trois majuscules) - les messages des champs numériques sont listés plus haut. Ce dernier message nomme l'attribut Devise, jamais currency_code. Un currency_code qui n'est pas une String - le littéral JSON false, par exemple - est refusé de la même façon : seuls une clé omise, un null explicite, ou une String vide ou blanche comptent comme une absence et retombent sur EUR. Sur PATCH, le code devise déjà enregistré survit à ce refus, qui annule l'écriture. Corrigez la charge utile et rejouez : rien n'a été créé ni modifié.
403 : scope insuffisant, ou action refusée dans l'état courant
Deux causes, un même statut et une même clé error: "forbidden".
Un token sans le scope write sur POST /quotes, PATCH /quotes/{id}, PATCH /quotes/{id}/transition ou POST /quotes/{id}/convert :
{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}
Et l'état du devis : convertir un devis qui n'est pas accepted, modifier un devis qui n'est plus draft ou supprimer un devis qui n'est ni draft ni cancelled. Le message est alors un texte technique en anglais, à ne pas afficher ni analyser. Relisez le devis (GET /api/v1/quotes/{id}) pour connaître son lifecycle_state et les transitions permises, puis reprenez le parcours au bon endroit.
404 : PDF pas encore disponible
GET /api/v1/quotes/{id}/download renvoie 404 (La ressource demandée est introuvable) tant qu'aucun PDF n'est attaché au devis - juste après une création ou une modification, le temps de la régénération. Réessayez après quelques secondes.
404 : devis hors de votre périmètre
Un identifiant inconnu, un devis situé dans un workspace auquel votre application n'a pas accès, ou une adresse IP hors de la liste autorisée du workspace renvoient 404 sur les endpoints qui visent un devis précis - jamais 403 : l'API ne révèle pas l'existence de ressources hors de votre périmètre (Conventions de l'API). Sur les endpoints de workspace (liste et création), la même adresse IP refusée renvoie 403.
Pages liées
- Émettre une facture - le parcours de la facture issue de la conversion
- Envoyer par email - transmettre le PDF du devis à votre client
- Référence API : lister les devis
- Référence API : créer un devis
- Référence API : transition de cycle de vie d'un devis
- Référence API : convertir un devis en facture
- Référence API : télécharger le PDF d'un devis