Send by email and track delivery
Some of your recipients stay outside e-invoicing networks: foreign customers, individuals, unconnected structures. For them, email remains the delivery channel. Scribee sends your invoices and quotes by email from the API, with the document attached, and tracks delivery address by address: delivered, opened, rejected. You know who received what, without any sending infrastructure on your side.
What Scribee does for you
- Message composition: subject generated from the document number (
Facture INV-2024-00156,Devis Q2024-0001), body in the resolved language, your custom message, and a single attachment: the Factur-X for an invoice that has one, the readable PDF otherwise. - Per-recipient tracking: each address (primary and copies) produces its own delivery record, whose status advances as delivery events occur, with no action on your side.
- Message language:
froren, defaulting to the client's registered communication language.
The flow
Eligible documents and states
- Invoice: any lifecycle state except
draft, as soon as its PDF is generated. The exact list isdeposited,received,available,taken_in_charge,approved,disputed,refused,payment_sent,collected,rejected, andcancelled. A draft, never. - Quote: states
sent,accepted,rejected, andconverted. Adraft,cancelled, orexpiredquote is refused. - In both cases, a buyer contact email must be available, even if you provide
to_emailexplicitly. For an invoice, the email carried by the document's buyer takes precedence; when it is blank, Scribee uses the default contact email of the customer linked to that invoice. For a quote, the email must be set on the document's buyer. This address is a precondition for sending, never an automatically added recipient: recipients of this call come exclusively fromto_emailandcc_emails. - The buyer's contact address is a frozen copy, taken from the client's record when the document was created. It never resyncs with the directory: see the 422 section below.
Manual sends through this endpoint remain separate from automatic delivery of the original requested at validation. For new automatic deliveries, changing the customer's contact afterwards does not send a second original when processing is retried. A failed attempt remains retriable. This protection does not cover older deliveries recorded without a link to their validation: check their history before retrying them.
An ineligible state or a PDF not yet generated is refused with a 403, before any payload validation.
Step 1: send an invoice
This call immediately queues a real email to each address provided: the 201 means the send is recorded and handed to the processing queue, not that it has left Scribee. Rendering and delivery happen afterwards, in the background, and can fail later - track status on email_deliveries rather than treating the 201 as a delivery receipt. Once gone, an email cannot be recalled. There is no test environment - to validate your integration, address the first send to an address you control. Sending changes neither the document's lifecycle nor its data, and transmits nothing to the PPF (Portail Public de Facturation) or the Peppol network. A read write token is required.
curl -X POST https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/send_by_email \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email_delivery": {
"to_email": "client@example.com",
"cc_emails": ["compta@example.com"],
"body_text": "Vous trouverez ci-joint votre facture.",
"locale": "fr"
}
}'
Only to_email is mandatory. 201 response, abridged to the useful fields: one delivery per recipient kept, all sharing the same send_batch_id. Every row in a batch carries the same subject, the same locale, and the same sent_at, whatever its recipient_type.
{
"data": [
{
"id": 87,
"send_batch_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient_type": "to",
"to_email": "client@example.com",
"subject": "Facture INV-2024-00156",
"locale": "fr",
"status": "queued",
"sent_at": "2026-07-31T11:12:04+02:00"
},
{
"id": 88,
"send_batch_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient_type": "cc",
"to_email": "compta@example.com",
"subject": "Facture INV-2024-00156",
"locale": "fr",
"status": "queued",
"sent_at": "2026-07-31T11:12:04+02:00"
}
]
}
Timestamps are serialized as ISO 8601 with the Europe/Paris offset (+02:00 in summer time, +01:00 in winter time), never in UTC with a Z suffix.
Three behaviors to know:
- The subject is not a parameter. It is generated from the document number, in the resolved language.
localeacceptsfroren; any other value falls back tofr. Without a value, the client's communication language applies,frby default.- Duplicate addresses are ignored (case-insensitive comparison, including the primary address): an address receives only one copy.
body_textaccepts a maximum of 5000 characters.
Step 2: track delivery
GET /api/v1/invoices/{id}/email_deliveries returns the invoice's send history, most recent first (sorted on created_at descending, not configurable), paginated with page and per_page. A read with no side effects; a read token is enough. per_page defaults to 20 and is silently clamped to the 1-100 range: per_page=500 returns 100 rows with no warning.
curl https://app.scribee.tech/api/v1/invoices/YOUR_INVOICE_ID/email_deliveries \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
200 response, abridged to the fields useful here. The objects returned are the same as those of step 1's POST: same fields, same shape.
{
"data": [
{
"id": 87,
"recipient_type": "to",
"to_email": "client@example.com",
"status": "opened",
"delivered_at": "2026-07-31T11:12:31+02:00",
"first_opened_at": "2026-07-31T12:02:11+02:00",
"last_opened_at": "2026-07-31T16:45:52+02:00",
"failed_reason": null
},
{
"id": 88,
"recipient_type": "cc",
"to_email": "compta@example.com",
"status": "delivered",
"delivered_at": "2026-07-31T11:12:35+02:00",
"first_opened_at": null,
"last_opened_at": null,
"failed_reason": null
}
],
"meta": { "current_page": 1, "per_page": 20, "total_pages": 1, "total_count": 2 }
}
The possible statuses:
| Status | Meaning | Associated field |
|---|---|---|
pending | send recorded, not yet queued | created_at |
queued | queued for sending | sent_at |
delivered | delivered to the recipient's mailbox | delivered_at |
opened | message opened | first_opened_at, last_opened_at |
clicked | a link in the message was followed | - |
bounced | rejected by the recipient's server | failed_reason |
failed | send failure | failed_reason |
blocked | address blocked at send time | failed_reason |
spam | reported as spam | - |
pending is the state deliveries are created in. It is replaced by queued (or by failed if queuing fails) right after, before the POST response returns, so the 201 never carries it. A concurrent GET issued during that window can observe it, though: treat it as a transient state, not an impossible one.
The status advances and never goes backward: opened does not revert to delivered, and bounced, failed, blocked, and spam are terminal. No webhook signals these changes: poll the endpoint at whatever rate suits your use case.
sent_at is stamped when the batch is handed to the mail provider, and it stays set whatever status the delivery reaches afterwards: a terminal delivery therefore carries a non-null sent_at, alongside the failed_reason that comes with bounced, failed, and blocked. A populated sent_at attests the handoff to the provider, not delivery to the recipient: delivered_at attests that.
Step 3: send a quote
The same two operations exist for quotes, with the same payload and the same responses: POST /api/v1/quotes/{id}/send_by_email and GET /api/v1/quotes/{id}/email_deliveries. The warning from step 1 applies identically: the email is queued immediately and goes out with no further confirmation.
curl -X POST https://app.scribee.tech/api/v1/quotes/YOUR_QUOTE_ID/send_by_email \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email_delivery": { "to_email": "client@example.com" } }'
The subject becomes Devis Q2024-0001; the rest of the behavior, including tracking statuses, is identical.
What the recipient receives
An email in the resolved language, carrying the document's information (number, issue date, due date or expiration date, total amount including tax) and your body_text presented as the sender's message.
The message carries a single attachment, and which one depends on the document:
- An invoice that has a Factur-X: that Factur-X, named
INV-2024-00156-facturx.pdf. A Factur-X is a PDF/A-3: it opens in any reader and carries the CII XML, so it fills both roles on its own. When the invoice was deposited with Scribee as a Factur-X, the deposited bytes are what leave, unchanged (Formats and downloads). - An invoice with no Factur-X, and every quote: the readable PDF, named after the document number (
INV-2024-00156.pdf). An invoice can legitimately be in this case - generation not finished yet, or thefacturxformat failed.
:::caution Behaviour change
Until now the message carried two attachments for an invoice with a Factur-X: the readable PDF and the Factur-X. The readable PDF is no longer attached in that case. If your processing relied on the <number>.pdf file being present, switch it to <number>-facturx.pdf, or fetch the format you want through the export endpoint, which still serves all four.
:::
What happens next
- The email leaves Scribee's infrastructure shortly after the
201response, once the rendering job has run; deliveries move fromqueuedto delivery statuses as events occur, with no call needed on your part. - After a
201, a delivery can flip tofailedwithfailed_reasonset todelivery_failed: rendering or sending failed in the background. That is the only internal code an accepted send can produce. A bounce from the recipient's server puts the delivery inbounced, and other events reported by the sending provider producefailedwith the reason they supply - but only if the delivery has not already reached a terminal status.bounced,failed,blocked, andspamall rank equally: the first to arrive freezes the status, and a later terminal event can only changefailed_reason. A delivery that was deferred and then bounced therefore staysfailed. Readfailed_reasonalongside the status. enqueue_failednever follows a201: it is written when queueing itself fails, and the call then answers422. The deliveries have already been created at that point, so the document's history shows them asfailedwith that code - treat them as not sent and replay the send. The history is specific to each document type:GET /api/v1/invoices/{id}/email_deliveriesfor an invoice,GET /api/v1/quotes/{id}/email_deliveriesfor a quote. A quote send never appears on the invoice endpoint.- Those two codes are deliberately opaque; the technical detail stays on Scribee's side. Contact support with the
send_batch_idif either persists. - Each new
POSTcall creates a new batch (send_batch_id) and new deliveries: resending a document is possible at any time, and the complete history remains available onemail_deliveries. - The document itself does not change: no lifecycle transition, no network transmission. Sending by email is independent of the regulatory channel described in The invoice lifecycle.
Errors and edge cases
400 Bad Request
Two cases, both in the usual error envelope: only the message differs.
A POST body with no email_delivery object never reaches validation: parameter parsing fails first, and the response is a 400 in the usual error envelope, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé", with no details key. Always send the payload wrapped in email_delivery.
On GET .../email_deliveries, a page higher than the number of available pages returns a 400 with the same bad_request key and a different message:
{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}
403 Forbidden: ineligible document
The document's state prohibits sending: invoice draft, quote draft, cancelled, or expired, or a PDF not yet generated. A 403 also signals a token without sufficient scopes - sending requires a read write token, reading deliveries a read token.
The response always carries "error": "forbidden", but the message differs depending on the cause and is not guaranteed to be translated: base your logic on the HTTP status and on error, never on the text. Check the document's state via GET /api/v1/invoices/{id} before sending.
404 Not Found
{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}
Four situations give this response, without telling them apart:
- The document does not exist.
- It belongs to a workspace not linked to your application.
- It belongs to a linked workspace whose IP allowlist excludes the address the call comes from. These paths carry no
workspace_id, so a scope refusal appears here as a404and not a403. - It is a sales invoice, or a quote, of a company whose Scribee offer does not cover sales. Purchase invoices are never hidden this way.
422 Unprocessable Entity: validation failed
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["L'adresse e-mail du destinataire est invalide"]
}
}
The possible messages under details.base:
| Message | Cause | What you do |
|---|---|---|
L'adresse e-mail du destinataire est invalide | to_email malformed or missing | fix the address |
L'adresse e-mail en copie est invalide : compta.exemple.com | a cc_emails entry is malformed (the first offending address is quoted) | fix or remove the entry |
Aucune adresse e-mail de contact n'est associée à l'acheteur | the document carries a buyer with no contact address | see below |
L'envoi de l'e-mail a échoué. Veuillez réessayer. | queuing failed; no delivery was sent | replay the call |
A body_text beyond 5000 characters also returns 422.
Address validation is permissive: it requires an @ and a domain part, but accepts a domain with no dot. compta@exemple therefore passes validation and the send is attempted.
Aucune adresse e-mail de contact n'est associée à l'acheteur: check the invoice contact or the linked customer's contact. For an invoice whose buyer has no contact_email, adding an email to the linked customer's default contact allows you to retry sending. This fallback does not change the data copied onto the invoice and never replaces an address already set there. Without a linked customer, fill in the buyer contact while the invoice remains editable. For a quote, the address is still the one copied onto the document: completing the customer's record afterwards does not update it. See Issue a sales invoice and Customers and suppliers.
401 Unauthorized
Missing, expired, or invalid token; empty body. Request a new token (Authentication).