Skip to main content

Receive supplier invoices

As of September 1, 2026, every company subject to VAT in France must be able to receive e-invoices: receiving is the first obligation of the reform, even before issuing. Your companies' purchase invoices are recorded in Scribee, your system reads them through the API, then drives the statuses on the buyer side - availability, approval, dispute, refusal, payment. A self-billed invoice, drawn up by your customer on your behalf and received over the Peppol network, is the exception: it is recorded as a sale (see step 1). This page covers the two calls that carry this flow: the invoice list and the lifecycle transition.

What Scribee does for you​

  • Origin channel tracked: every recorded invoice carries the channel through which it entered the workspace, in the upload_source field:
    • api: a file posted to the import through the API endpoint.
    • email: an invoice received by email.
    • supplier_portal: deposited by the supplier on their portal.
    • chorus: an invoice received from Chorus Pro, the public-sector invoicing portal; it is recorded as a purchase.
    • manual: none of these channels. An invoice built field by field on POST /api/v1/workspaces/{workspace_id}/invoices carries manual, and so does an invoice received over the Peppol network.
  • AIFE lifecycle (Agence pour l'informatique financière de l'État): every status change is a timestamped, auditable event, with its standardized reason and, for a message coming from the network, the role of the sending party plus the identifier of the selected recipient platform, retrievable via include=lifecycle_events.
  • Notifications: Webhooks push the arrival of an invoice (invoice.created) and every status change (invoice.lifecycle_event.created) to your system.

:::caution The invoice.created payload is a snapshot taken at creation time On an invoice ingested from a file (API import, email, supplier portal), the snapshot is serialized before the origin channel is recorded: the payload therefore carries upload_source: "manual" whatever the real channel was. Do not rely on that field in the webhook - read the invoice back at GET /api/v1/invoices/{id} for its final value. The invoice is also still in the draft state there, so lifecycle_available_transitions lists only the transitions open from that state. :::

The journey of a received invoice​

A Peppol reception without the buyer's SIREN is refused​

An invoice reaching you over the Peppol network, issued by a supplier established in France, is refused when the buyer does not state its legal identifier (BT-47) on it. The refusal happens before anything is written: no invoice is created, nothing enters the workspace, and neither the list nor the webhooks carry a trace of it. The rule is identified as SCRIBEE-BR-FR-11. It covers that reception only: an invoice you create, import, or upload through the API is never subject to it.

What the invoice must carry is the buyer's SIREN in its legal identifier, under scheme 0002 and made of exactly 9 digits: in UBL, cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID with schemeID="0002"; in CII, including the CII embedded in a Factur-X, ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:ID under the same scheme.

A SIREN readable anywhere else does not satisfy the rule. A buyer identified only by its intra-community VAT number (BT-48, FR69572053833) or only by its electronic address (BT-49, scheme 0225) is refused, even though the nine digits of its SIREN appear in those values. That is a decision taken in full knowledge of what it rejects, not an oversight: the AFNOR XP Z12-012 standard, Annexe A, makes the buyer's SIREN mandatory for invoices within the e-invoicing perimeter, and it makes it mandatory in BT-47.

A supplier established outside France is not concerned. Its invoice to a French buyer is an intra-community acquisition, outside the e-invoicing perimeter; it is received normally. Scribee reads where the seller is established from the identifiers the invoice gives it, in this order: a legal registration under a French scheme (0002 SIREN or 0009 SIRET) that reduces to a SIREN marks it as French; a legal registration under a foreign scheme marks it as foreign, even when it also carries a French VAT number; with no legal registration at all, the VAT number answers.

What you have to do: ask your French suppliers to fill in the buyer's SIREN in BT-47. Carrying it in the VAT number or in the electronic address will not do.

A Peppol reception addressed to another company is refused​

An invoice received over the Peppol network is also refused when its addressee - the buyer, or the seller on a self-billed invoice - contradicts the Peppol participant it was sent to. Only a stated contradiction counts: the invoice is refused when it carries, for that addressee, an electronic address or a legal identifier under the same scheme as the participant's Peppol identifier, and none of those values is the participant's. An identifier that is absent, or stated under another scheme, does not get the invoice refused.

The refusal happens before anything is written: no invoice is created, no webhook is sent, no lifecycle event is recorded. It is the sending platform that is informed, through status 221 Routing error.

Step 1: detect a new supplier invoice​

Two options: Webhooks (the invoice.created event pushes the invoice as soon as it is recorded), or periodically polling the list. The list returns invoices in both directions - direction is either purchases (Purchases) or sales (Sales) on each item. Purchase invoices are always returned; sales invoices are returned only for companies whose workspace covers sales, otherwise they are absent from the list with no error - except self-billed invoices received over Peppol, described below. It accepts, alongside pagination (page, per_page), sorting (sort_by, sort_order), and include, six filters: created_at_from, created_at_to, updated_at_from, updated_at_to, lifecycle_status, and company_ids (array). lifecycle_status expects the state name, not the numeric code: draft, deposited, received, available, taken_in_charge, approved, disputed, refused, payment_sent, collected, rejected, or cancelled - a single name or an array of names. Any other value, including a code such as 203, responds 400 with "Invalid lifecycle_status value(s). Allowed values: ...". To detect new arrivals, sort by descending creation date and stop at the first already-known identifier. This call is a read: no side effect, replayable at will.

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/invoices?sort_by=created_at&sort_order=desc" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Abridged response, fields useful to a purchases flow:

{
"data": [
{
"id": 12401,
"invoice_number": "FAC-2026-0341",
"issue_date": "2026-07-21",
"due_date": "2026-08-20",
"direction": "purchases",
"type_code": "invoice",
"proforma": false,
"upload_source": "api",
"seller": { "party_id": 87, "name": "Fournitures Pro SARL" },
"tax_inclusive_amount": 1770.0,
"lifecycle_state": "available",
"lifecycle_status_code": "203",
"lifecycle_available_transitions": ["take_in_charge", "approve", "dispute", "refuse", "send_payment", "revert_to_draft"]
},
{
"id": 12398,
"invoice_number": "INV-2026-00212",
"direction": "sales",
"lifecycle_state": "deposited",
"lifecycle_status_code": "200"
}
],
"meta": { "current_page": 1, "per_page": 20, "total_pages": 8, "total_count": 156 }
}

On a purchase invoice, the supplier is the seller party. lifecycle_available_transitions lists the events your system can trigger from the current state: read it before every transition rather than recomputing the state machine on your side. An invoice's detail, lines included, is read at GET /api/v1/invoices/{id} with the same include parameter (API conventions).

A self-billed invoice (type_code in self_billed_*, BT-3 codes 389, 501, 500, 471, 473, 261 and 502) is drawn up by your customer on your behalf. Received over the Peppol network, it is recorded as a sale: direction is sales, your company is its seller party, and the invoice enters directly at 203 Available, without going through 200 Deposited. It is returned by the list and readable at GET /api/v1/invoices/{id} even when the company's workspace does not cover sales. From 203, the only open transition is collect (see The invoice lifecycle). Other invoices received over Peppol remain purchase invoices.

:::caution A proforma invoice is not an invoice When Scribee recognises, on reading a document deposited as a purchase, a proforma invoice from your supplier, it keeps it as a purchase with proforma set to true. It is not an invoice: Scribee generates no accounting entry for it, includes it in no accounting export, and the final invoice that replaces it is not treated as its duplicate. Do not book or pay it as an invoice: the final invoice, deposited later, is the one that counts. proforma is false on every other invoice and cannot be written through the API. :::

Step 2: validate an imported invoice (203 Available)​

An invoice you import, that arrives by email, or that is deposited on the supplier portal starts at 000 Draft: the make_available event brings it into the lifecycle, directly at 203 Available - status 200 Deposited is specific to the Sales direction. On a purchase invoice, make_available starts from 000 Draft - the case for your imports - and also from 202 Received, the state the platform puts an invoice in when it arrives over the regulatory network.

This call commits the invoice to the regulatory lifecycle; it stops being editable. Nothing is sent to the supplier. If the company has connected its accounting software with automatic delivery from the Scribee interface, the validated invoice is transmitted to it. An imported invoice can revert to draft via the revert_to_draft event; read lifecycle_available_transitions to know whether the event is open on a given invoice.

make_available is refused as long as the invoice carries a blocking import error. That is the case for an imported file missing a mandatory field - invoice number, issue date, document type, or currency: the import succeeds, the invoice is created at 000 Draft with a placeholder value in the missing field, and the transition is then refused. The message returned is the impossible-transition one (Impossible de passer de draft à make_available...), which does not name that cause, and the API does not expose an invoice's import errors. Correct the invoice with PATCH /api/v1/invoices/{id}: a successful update clears the import errors and unblocks make_available.

curl -X PATCH https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "invoice": { "event": "make_available" } }'
{
"data": {
"lifecycle_state": "available",
"lifecycle_status_code": "203",
"lifecycle_available_transitions": ["take_in_charge", "approve", "dispute", "refuse", "send_payment", "revert_to_draft"]
}
}

Step 3: take in charge (204 Taken in charge) - optional​

Taking in charge signals that the invoice has entered your internal processing circuit. The status is optional: you can approve, dispute, or refuse directly from 203. It is recorded in Scribee and notified by webhook. Same call as above with "event": "take_in_charge"; from 204, the available transitions are approve, dispute, refuse, and send_payment.

Step 4: approve (205 Approved)​

Approval is the standard outcome after verifying the invoice. It is triggered from 203, 204, or 207, and opens the way to payment.

curl -X PATCH https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "invoice": { "event": "approve" } }'
{
"data": {
"lifecycle_state": "approved",
"lifecycle_status_code": "205",
"lifecycle_available_transitions": ["send_payment"]
}
}

Step 5: dispute or refuse (207 Disputed, 210 Refused)​

A dispute or a refusal concerns a real invoice, on your production account, and leaves a permanent trace: the status, its standardized reason, and its timestamp are recorded in the invoice's auditable history. Both are recorded in Scribee and notified to your system by webhook; the call sends nothing to the supplier. The two differ in reversibility: from 207, you can still approve or refuse, whereas refusal is final - no transition leaves the Refused state. The only exception, outside the API: a Scribee user can reopen from the interface a purchase invoice whose refusal never left Scribee; it then goes back to available with no lifecycle event and no webhook (see Reopening a refused purchase invoice).

Both require a standardized reason code (reason_code), specific to each status; the list of valid codes per status is on The invoice lifecycle. The free-text reason (250 characters maximum) is optional, except for the AUTRE and REF_ERR codes, which require it.

curl -X PATCH https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"invoice": {
"event": "dispute",
"reason_code": "TX_TVA_ERR",
"reason": "Taux de TVA de la ligne 2 : 20 % attendu, 5,5 % facturé"
}
}'
{
"data": {
"lifecycle_state": "disputed",
"lifecycle_status_code": "207",
"lifecycle_available_transitions": ["approve", "refuse"]
}
}

Refusal follows the same form, with a code from the set specific to status 210:

curl -X PATCH https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "invoice": { "event": "refuse", "reason_code": "DOUBLON" } }'
{
"data": {
"lifecycle_state": "refused",
"lifecycle_status_code": "210",
"lifecycle_available_transitions": []
}
}

Step 6: declare the payment (211 Payment sent)​

From 203 Made available, 204 Taken in charge, or 205 Approved, send_payment records that the payment has been issued to the supplier. Taking in charge and approval are optional: you can declare the payment of an invoice you have not approved. On an account where the purchase invoice approval workflow is enabled, however, send_payment is only offered from 205. Collection (212 Collected) is not triggered through this endpoint: it is set by Scribee when the invoice's settled amount reaches the tax-inclusive total - see Record payments.

curl -X PATCH https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/transition \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "invoice": { "event": "send_payment" } }'
{
"data": {
"lifecycle_state": "payment_sent",
"lifecycle_status_code": "211",
"lifecycle_available_transitions": []
}
}

lifecycle_available_transitions is empty: what comes next (212 Collected) is set by Scribee from the settled amount, not through a direct transition.

What happens next​

  • Every transition creates a lifecycle event on the invoice: status, label, date, and reason, retrievable via include=lifecycle_events on the list as well as on the detail view. The event does not identify its author, and its three sender_* fields do not all describe the same party. For a status message received from the network, sender_role repeats the role of the message's sending party, while sender_id and sender_scheme_id identify the selected recipient platform (the recipient party with role WK) - not the sender. Those last two are null when the message carries no recipient party with that role. All three stay null for a transition you trigger through the API. This is the reform's auditable history; it is never erased.
  • An invoice.lifecycle_event.created webhook is emitted for every event - see Webhooks.
  • lifecycle_available_transitions only contains the events your system can trigger. receive, reject, and cancel never appear there - but the processing of inbound regulatory flows does apply them: statuses 202 Received, 213 Rejected, and 220 Cancelled can therefore genuinely appear in the history, without ever having been offered in lifecycle_available_transitions. Treat lifecycle_state as an open value, with a default case for an unknown state.
  • Every status you trigger - taking in charge (204), approval (205), dispute (207), refusal (210), payment sent (211) - is recorded in Scribee and notified by webhook.

Errors and edge cases​

All transition errors share the 422 format: error is unprocessable_entity, code carries the machine identifier of the failure class, message describes the cause in French, and details stays empty on a purchase invoice. Each cause has its own message: one refusal never borrows another's text. Branch on the status and on code, not on the text. The general error format applies otherwise.

422: unknown event​

{
"error": "unprocessable_entity",
"code": "invalid_argument",
"message": "Évènement de cycle de vie inconnu ou manquant. Fournissez un nom d'évènement valide parmi les transitions disponibles de la facture.",
"details": {}
}

The event field is missing or does not match any event. Send one of the names returned in lifecycle_available_transitions. The value must be a string: a number or a JSON object does not produce this 422 but a server error.

422: transition impossible from the current state​

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Impossible de passer de refused à approve. L'état actuel ne permet pas cette transition.",
"details": {}
}

The event exists but the current state does not allow it - here, an approval after a refusal, which is final. Reread the invoice and rely on lifecycle_available_transitions, which is authoritative.

This message is only ever about the state. A business check that blocks a transition the current state does allow now has its own message: that is the next case.

422: blocking import errors​

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "La facture comporte des erreurs d'import bloquantes. Corrigez-les avant de changer son statut.",
"details": {}
}

make_available on a purchase draft stays refused as long as the invoice carries a blocking import error - typically an invoice that arrived without invoice_number, issue_date, type_code, or currency_code, and whose missing field Scribee filled with a placeholder. A PATCH carrying the whole invoice clears those errors and unblocks the transition. The two other deposit checks (Factur-X conformance of a supplied PDF, mandatory legal mentions) only apply to sales invoices and never appear on this path - see The invoice lifecycle.

422: transition reserved for the system​

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "La transition cancel ne peut pas être déclenchée manuellement.",
"details": {}
}

receive, reject, and cancel exist in the state engine, but no API call can trigger them, whatever the invoice's direction; Sales-direction events (deposit, collect) are refused the same way on a purchase invoice. You get this message when the current state would allow the transition, and the impossible-transition message above otherwise. The events you can trigger on the purchase side: make_available, take_in_charge, approve, dispute, refuse, send_payment, revert_to_draft.

422: missing or invalid reason​

{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "Un motif normalisé est requis pour le statut Refusée",
"details": {}
}

dispute and refuse require a reason_code. Two variants of the same check: a code outside the target status's set returns Le motif DEST_INC n'est pas autorisé pour le statut Refusée, and an AUTRE or REF_ERR code with no free text returns Un commentaire est requis pour le motif AUTRE. The list of codes by status is on The invoice lifecycle.

404: invoice not found​

The identifier does not exist, or the invoice belongs to a workspace outside your application's scope - both cases return the same response, the API does not reveal the existence of resources outside your scope (conventions).

403: insufficient scope​

The transition requires the write scope: the token must carry it, and read write is the combination to request. Reading the list, for its part, requires read. Request a new token with the right scope (Authentication).