Skip to main content

Quotes

The quote covers the commercial phase that precedes the invoice: your system proposes a service, the client accepts or rejects it, and acceptance turns into a sales invoice without re-entry. The API exposes this journey end to end - creation, finalization, client decision, conversion - with an explicit lifecycle state at each step. The quote is not a regulated document: Scribee produces it as a PDF, without transmission to the PPF (Portail Public de Facturation) or over the Peppol network.

What Scribee does for you​

  • Numbering: the draft carries a provisional number prefixed DRAFT-; the final number is assigned at finalization, from the numbering pattern configured on the company from the Scribee interface, with a sequence that increments with each quote and never resets. A quote_number sent in the payload is ignored.
  • Amount calculation: you provide the lines (quantity, unit price, VAT rate, discounts); Scribee calculates the totals excluding tax, VAT, and including tax, and the subtotals per rate.
  • Payment means and legal mentions: they are taken from the issuing company's configuration (bank accounts, late-payment, recovery-cost and cash-discount disclaimers, legal form and share capital). They are not driven from the API payload; you read them back on the created quote with include=payment_means,item_notes.
  • PDF: the PDF document is generated at creation and regenerated on every draft modification and at finalization; you retrieve it from GET /api/v1/quotes/{id}/download.
  • Automatic expiration: once a day, any sent quote whose expiration date has passed moves to expired, with no action on your part.
  • Conversion into an invoice: POST /api/v1/quotes/{id}/convert clones the quote - parties, lines, discounts, notes, VAT subtotals, payment means - into a draft sales invoice and links the two documents.

The lifecycle​

Seven states, seven labels:

API codeLabel
draftDraft
sentSent
acceptedAccepted
rejectedRejected
expiredExpired
cancelledCancelled
convertedConverted

Transitions are triggered on PATCH /api/v1/quotes/{id}/transition, with a body {"quote": {"event": "..."}} and the write scope. The six events - finalize, accept, reject, expire, cancel, convert - are all triggerable through the API, without restriction: unlike the invoice, where some lifecycle statuses are reserved for the platform or the recipient (The invoice lifecycle), the quote is entirely driven by your system.

Each response exposes lifecycle_available_transitions, the list of events actually triggerable from the current state. That list accounts for preconditions, not only for the state: a draft with no expiration_date returns ["cancel"], without finalize.

A vocabulary note for your screens: a rejected quote is labeled Rejected; on an invoice, status 210 is labeled Refused and 213 Rejected - three distinct notions, not to be confused.

rejected, expired, cancelled, and converted are terminal states: no transition leaves them. Deletion (DELETE /api/v1/quotes/{id}, write scope) is possible only in draft and cancelled; the other states keep the document.

List the quotes​

GET /api/v1/workspaces/{workspace_id}/quotes returns the workspace's quotes, read scope. The list is paginated (page, per_page - 20 by default, 100 at most) and sortable (sort_by: issue_date, expiration_date, quote_number, tax_inclusive_amount, total_amount_excluding_taxes, lifecycle_state, created_at; sort_order: asc or desc; default sort: issue_date descending). An unknown sort value is ignored and the default sort applies.

It exposes no filter at all: no lifecycle_state, no date range, no company, no customer, no search. Do not plan for request-side filtering, unlike the invoice list; filter after receiving, or paginate through the whole set.

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"

The response carries data (the array of quotes) and meta (current_page, per_page, total_pages, total_count). include accepts the same values as a single read: lines, payment_means, tax_subtotals, item_notes; an unknown value is ignored without error.

Step 1: create the draft​

This call creates a quote in the draft state in your workspace. Nothing goes out to the client or to any network, and the draft can be deleted at any time: the operation is entirely reversible. The write scope is required. company_id designates the issuing company, customer_id a client of that company (Customers and suppliers) whose details are copied into the quote.

Accepted header fields: company_id, customer_id, billing_address_id, issue_date, expiration_date, currency_code, global_discount_value, global_discount_type, global_discount_vat_rate, lines. An omitted issue_date becomes today, an omitted currency_code becomes EUR. When filled in, currency_code must appear in the ISO 4217 list Scribee ships, at the exact case: EUR passes, eur and USd are refused with a 422, as is a well-shaped token that names no currency. No validation rejects an expiration_date in the past: the quote then moves to expired on the next run of the expiration task.

billing_address_id selects which of the customer's billing addresses is copied into the buyer block. An unknown ID, or one that does not designate a billing address of that customer, does not return an error: the default billing address is used instead. Read the buyer block of the response back to check which address was used.

The two line shapes​

A line is either catalog or freeform; the presence of product_id decides which.

  • Catalog line (product_id set): the name, unit and unit price come from the product. An article_name or a unit_price sent on that line is silently ignored. Only vat_rate overrides the product's rate. A product_id that does not designate a sellable product of the company returns 422 (Produit introuvable.).
  • Freeform line (product_id absent): article_name and unit_price are required. The unit is always C62 (piece) and the VAT rate is 20 when vat_rate is absent.

A line with neither product_id nor article_name is silently discarded. It raises no error: it simply does not appear in the quote. If every line is discarded, creation fails with 422 (Au moins une ligne de devis est requise.).

Each line also accepts vat_rate, note, note_type (general_information by default), discount_value and discount_type. quantity is required on every retained line, catalog or freeform: it is the only numeric field with no default, and its absence is refused with a 422.

Discounts​

A discount is set either at document level (global_discount_value, global_discount_type, global_discount_vat_rate) or at line level (discount_value, discount_type).

Only the exact string percentage is read as a percentage. Any other value of global_discount_type or discount_type - fixed, percent, %, an empty string, the field absent - makes the value an absolute amount in the document currency. A discount sent with "discount_type": "percent" and "discount_value": 10 therefore takes off 10 currency units, not 10%.

A discount whose value is zero is ignored. global_discount_vat_rate is accepted by the payload but never read: the VAT rate of a document discount is derived from the lines it covers, and the discount is spread across the VAT categories present in proportion to their base.

:::note Numeric fields are checked before any write A non-numeric value in quantity, unit_price (freeform line), vat_rate, discount_value or global_discount_value is refused with a 422, with a message naming the field at fault. An absent quantity is refused the same way. Nothing is created or modified. This applies to POST as well as PATCH.

Two values escape the check because they are never converted: the unit_price of a catalog line, ignored in favor of the product's price, and the fields of a discarded line. :::

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
}
]
}
}'

201 response, trimmed to the fields useful here. The real response additionally carries the seller and buyer blocks, the seven monetary totals, company_id, created_at and 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
}
}

The draft is read back with GET /api/v1/quotes/314?include=lines (include values: lines, payment_means, tax_subtotals, item_notes) and modified with PATCH /api/v1/quotes/314 as long as it is in draft; every modification regenerates the PDF.

:::caution PATCH replaces the quote, it does not merge into it The PATCH body is treated as a complete quote. Every call destroys and rebuilds the lines, discounts, expenses, VAT breakdowns, payment means, notes and parties: always send company_id, customer_id and the entire lines array, even to change a single field. Omitted attributes are reset - issue_date becomes today, expiration_date becomes null and currency_code reverts to EUR. A currency_code present but outside the ISO 4217 list fails the entire replacement with a 422: nothing is destroyed or rebuilt, the quote stays as it was. A partial body answers 422 ("Entreprise introuvable."). :::

Step 2: finalize​

finalize locks the quote and assigns it its final number. This call does not transmit anything to the client - sending the PDF is a separate call (Send by email and track delivery) - but it consumes a number in the company's sequence, which never goes back, and the quote leaves the draft state for good. Two preconditions: an expiration_date set, and a quote numbering pattern configured on the company from the Scribee interface.

:::warning A finalize refused for a missing expiration_date still consumes a number The final number is reserved before the expiration_date is checked. A finalize on a draft with no expiration date does return 422 and leaves the quote in draft, but the company's sequence has already been incremented: that number is lost and your quote numbering will show a gap. Every further attempt consumes one more.

Check lifecycle_available_transitions before calling: as long as finalize is not in it, the precondition is not met. Set the date first with a complete PATCH (see the box above), then finalize. :::

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"}}'

200 response, trimmed: as at creation, the real response carries the full set of quote fields.

{
"data": {
"id": 314,
"quote_number": "DEV-2026-07-42",
"lifecycle_state": "sent",
"lifecycle_available_transitions": ["accept", "reject", "expire", "cancel"]
}
}

Step 3: record the client's decision​

The client replies outside the API - it is your system that records their decision. This call only changes the quote's state, nothing goes out externally, and the PDF is not regenerated. accept opens up conversion into an invoice; reject, cancel, and expire close the quote for good.

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"}}'

200 response, trimmed:

{
"data": {
"id": 314,
"lifecycle_state": "accepted",
"lifecycle_available_transitions": ["convert"]
}
}

Step 4: convert into an invoice​

The conversion creates a sales invoice in the draft state in the same workspace, a line-by-line copy of the quote, and moves the quote to converted - a terminal state: a quote converts only once. Nothing is transmitted: the invoice starts as a draft, and the deposit itself sends nothing to the PPF or over the Peppol network (Issue a sales invoice). Only an accepted quote converts. The write scope is required.

:::danger Never use the convert event on the transition endpoint PATCH /api/v1/quotes/{id}/transition with {"quote": {"event": "convert"}} moves the quote to converted without creating any invoice. converted is terminal: the quote has no transition left, and POST /api/v1/quotes/{id}/convert then answers 403 permanently, since it requires the accepted state. There is no way back and no way to recover the invoice. Always go through POST /api/v1/quotes/{id}/convert. :::

curl -X POST https://app.scribee.tech/api/v1/quotes/314/convert \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

201 response: the quote, with the ID of the created invoice in converted_invoice_id. The response is the quote, trimmed here to the fields useful for this step.

{
"data": {
"id": 314,
"quote_number": "DEV-2026-07-42",
"lifecycle_state": "converted",
"lifecycle_available_transitions": [],
"converted_invoice_id": 9021
}
}

The created invoice is not a verbatim copy of the quote on three points, worth knowing if your system reconciles the two documents:

  • its issue_date is the date of the conversion, not the quote's issue date;
  • it carries a provisional number, like any draft invoice; the final number is assigned at deposit;
  • its customization_id is set to the Factur-X EXTENDED profile. An invoice you create directly through the API only stores the customization_id you send; with no value sent, the field stays null, but the produced files carry the same EXTENDED-CTC-FR profile (Formats and downloads).

A VAT breakdown in category E - the VAT franchise (article 293 B of the CGI), which a quote drawn up in the Scribee application can state - reaches the invoice with its exemption reason: in the invoice's tax_subtotals, tax_exemption_reason_code is VATEX-FR-FRANCHISE and tax_exemption_reason gives its wording. A quote created through the API states no category: its breakdowns are in S, or in Z at a zero rate.

The rest of the journey - reading back the invoice draft, deposit, AIFE (Agence pour l'informatique financière de l'État) statuses - plays out on invoice 9021: Issue a sales invoice.

Step 5: download the PDF​

Read with no side effect, available in any state as soon as the PDF is generated, with the read scope. The response is a 302 redirect to a Scribee download URL valid for 5 minutes at most: it stops working earlier if the quote's PDF is regenerated in the meantime (draft modification, finalize). Follow it immediately with -L, without keeping it. Once expired or invalidated, it answers 404; call the endpoint again to get a new one. The file is served by Scribee, never through a direct link to third-party storage.

curl -L https://app.scribee.tech/api/v1/quotes/314/download \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-o devis-DEV-2026-07-42.pdf

What happens next​

  • Automatic expiration: once a day, Scribee moves any sent quote whose expiration_date has passed to expired. If your system tracks quotes in progress, expect to see this state appear with no call on your part.
  • PDF generation: it is asynchronous. After a creation or a modification, the download may respond 404 for a few moments while the new PDF is produced - a modification purges the old PDF before producing the new one, so the unavailability window is real, not just cache latency. Finalization behaves differently: it purges nothing. The transition response already carries the final number while the download still serves the previous PDF, the one showing the draft number. Wait until the downloaded PDF carries the number returned by the transition before sending it to the customer.
  • Conversion webhook: the invoice created by the conversion emits the invoice.created event to your subscribed endpoints (Webhooks), like any invoice creation. The quote itself emits no webhook: not its creation, not its transitions, not its deletion.
  • No regulatory exchange: neither the quote nor its conversion triggers any transmission; the AIFE circuit starts at the deposit of the invoice resulting from the conversion.

Errors and edge cases​

422: invalid numeric value​

The fields listed in the step 1 box are checked before any write, on creation as well as on modification. A non-numeric value returns a 422 whose details.base names the field: 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. or La remise globale doit être un nombre.. An absent quantity returns La quantité est requise pour les lignes de devis.. The quote is neither created nor modified.

The check runs after those on the product and on the freeform unit price: a payload carrying both faults returns Produit introuvable. or Le prix unitaire est requis pour les lignes libres. first.

400: missing quote envelope or page out of range​

Two cases, both in the usual error envelope. A body without the top-level quote envelope is rejected with 400 before it reaches the quote logic, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé". And on the list, a page number beyond the last one returns 400 with the same bad_request key:

{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}

422: transition not possible from the current state​

Triggering a disallowed event - for example accept on a draft - returns:

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Impossible de accept un devis à l'état draft : Event 'accept' cannot transition from 'draft'.",
"details": {}
}

Read lifecycle_available_transitions before triggering an event, and base your handling on the status and the error key, not on the message text (API conventions). A finalize without an expiration_date set fails the same way - but it has already consumed a sequence number, see the step 2 warning. An unknown event name returns the same status with the message Événement de transition invalide : approve..

422: no numbering pattern​

finalize fails as long as the company has no quote numbering pattern:

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Impossible de finaliser : l'entreprise n'a pas de format de numérotation de devis configuré.",
"details": {}
}

The pattern is configured in the company settings, from the Scribee interface. Once the pattern is in place, replay the finalize: the quote has not changed state, and no sequence number was consumed in this particular case.

422: payload validation​

Applies to creation as well as modification. The payload requires an existing company and client, at least one retained line, and - if you fill it in - a currency_code that appears in the ISO 4217 list. The body carries "message": "La validation a échoué" and the detail in details.base:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Au moins une ligne de devis est requise."]
}
}

The other details.base values on this journey: Entreprise introuvable. (company_id absent, unknown, or company not enabled for sales invoicing), Client introuvable. (customer_id absent or foreign to that company), Produit introuvable. (product_id of a catalog line), Le prix unitaire est requis pour les lignes libres. and Devise ne fait pas partie de la liste des devises ISO 4217 (currency_code missing from the list, or in a case other than the three upper-case letters) - the numeric-field messages are listed above. That last message names the Devise attribute, never currency_code. A currency_code that is not a String - the JSON literal false, for example - is refused the same way: only an omitted key, an explicit null, or a blank-or-whitespace String counts as absence and defaults to EUR. On PATCH, the currency code already stored survives that refusal, which rolls the write back. Fix the payload and replay: nothing was created or modified.

403: insufficient scope, or action denied in the current state​

Two causes, one status and one error: "forbidden" key.

A token without the write scope on POST /quotes, PATCH /quotes/{id}, PATCH /quotes/{id}/transition or POST /quotes/{id}/convert:

{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}

And the quote's state: converting a quote that is not accepted, modifying a quote that is no longer draft, or deleting a quote that is neither draft nor cancelled. The message is then a technical English string, not to be displayed or parsed. Read the quote back (GET /api/v1/quotes/{id}) to find its lifecycle_state and the allowed transitions, then resume the journey at the right point.

404: PDF not yet available​

GET /api/v1/quotes/{id}/download returns 404 (La ressource demandée est introuvable) as long as no PDF is attached to the quote - right after a creation or a modification, while it regenerates. Retry after a few seconds.

404: quote outside your scope​

An unknown ID, a quote located in a workspace your application does not have access to, or an IP address outside the workspace's allowlist all return 404 on the endpoints that target one specific quote - never 403: the API does not reveal the existence of resources outside your scope (API conventions). On the workspace endpoints (list and create), the same refused IP address returns 403.