Skip to main content

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: fr or en, 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 is deposited, received, available, taken_in_charge, approved, disputed, refused, payment_sent, collected, rejected, and cancelled. A draft, never.
  • Quote: states sent, accepted, rejected, and converted. A draft, cancelled, or expired quote is refused.
  • In both cases, a buyer contact email must be available, even if you provide to_email explicitly. 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 from to_email and cc_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.
  • locale accepts fr or en; any other value falls back to fr. Without a value, the client's communication language applies, fr by default.
  • Duplicate addresses are ignored (case-insensitive comparison, including the primary address): an address receives only one copy. body_text accepts 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:

StatusMeaningAssociated field
pendingsend recorded, not yet queuedcreated_at
queuedqueued for sendingsent_at
delivereddelivered to the recipient's mailboxdelivered_at
openedmessage openedfirst_opened_at, last_opened_at
clickeda link in the message was followed-
bouncedrejected by the recipient's serverfailed_reason
failedsend failurefailed_reason
blockedaddress blocked at send timefailed_reason
spamreported 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 the facturx format 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 201 response, once the rendering job has run; deliveries move from queued to delivery statuses as events occur, with no call needed on your part.
  • After a 201, a delivery can flip to failed with failed_reason set to delivery_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 in bounced, and other events reported by the sending provider produce failed with the reason they supply - but only if the delivery has not already reached a terminal status. bounced, failed, blocked, and spam all rank equally: the first to arrive freezes the status, and a later terminal event can only change failed_reason. A delivery that was deferred and then bounced therefore stays failed. Read failed_reason alongside the status.
  • enqueue_failed never follows a 201: it is written when queueing itself fails, and the call then answers 422. The deliveries have already been created at that point, so the document's history shows them as failed with 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_deliveries for an invoice, GET /api/v1/quotes/{id}/email_deliveries for 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_id if either persists.
  • Each new POST call creates a new batch (send_batch_id) and new deliveries: resending a document is possible at any time, and the complete history remains available on email_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 a 404 and not a 403.
  • 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:

MessageCauseWhat you do
L'adresse e-mail du destinataire est invalideto_email malformed or missingfix the address
L'adresse e-mail en copie est invalide : compta.exemple.coma 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'acheteurthe document carries a buyer with no contact addresssee below
L'envoi de l'e-mail a échoué. Veuillez réessayer.queuing failed; no delivery was sentreplay 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).