Proformas
Send a customer a formal quote with no fiscal validity, then turn it into a real invoice once they accept — as a draft or issued in the same call.
A proforma is a formal quote: it looks like an invoice (lines, totals, taxes, recipient, PDF) but it has no fiscal validity. It never enters VeriFactu, carries no QR, is never submitted to the AEAT and does not count against your invoice quota.
There is no separate proforma resource. A proforma is an invoice with type: "PROFORMA",
so you create, read, edit, list and delete it through the regular invoice endpoints. The
only operation of its own is the conversion into a real invoice.
Lifecycle
A proforma does not go through the draft → issue cycle of a fiscal invoice. It has one working state:
| Status | Meaning |
|---|---|
ACTIVE | The state it is born in, already numbered from a non-fiscal series (PRO-2026-0001). Editable and deletable. |
EXPIRED | An ACTIVE proforma whose valid_until has passed. Derived when you read it, never stored: underneath it is still ACTIVE, so it stays editable and convertible. Moving valid_until to a future date shows it as ACTIVE again. |
CONVERTED | Turned into an invoice. Terminal: no longer editable or deletable. |
VOIDED | The offer was rejected or withdrawn. Kept on record. |
valid_until is purely informational. Nothing happens automatically when it passes.
Create a proforma
Send type: "PROFORMA" to the normal create endpoint. Like a STANDARD invoice it needs
full recipient data: a registered customer, or an ad-hoc recipient with legal name, tax ID
and address. It is refused with the same errors as a STANDARD invoice: no recipient at
all answers 422 VALIDATION_ERROR (error.details.recipient), and an incomplete ad-hoc
recipient 422 RECIPIENT_ID_REQUIRED, RECIPIENT_FISCAL_NAME_REQUIRED or
RECIPIENT_ADDRESS_REQUIRED.
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/invoices" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "PROFORMA",
"valid_until": "2026-10-31",
"recipient": {
"customer_id": "4f244735-980b-8d9c-80e8-6331fa0b1958"
},
"lines": [
{
"description": "Corporate website redesign",
"quantity": 1,
"unit_price": 2400,
"main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" }
}
],
"notes": "Quote valid until 31 October."
}'The response comes back with status: "ACTIVE" and its PRO-... number already assigned.
options.issue_directly is ignored: there is no issue step for a proforma.
What a proforma cannot do
A proforma is not a fiscal document, so every fiscal or payment step is refused with its own error code:
| Attempt | Error |
|---|---|
| Issue it | 422 PROFORMA_NOT_ISSUABLE |
| Schedule it for later issuance | 400 PROFORMA_SCHEDULE_FORBIDDEN |
| Mark it as paid | 400 STATUS_NOT_MODIFIABLE |
| Mark it as sent | 400 MARK_SENT_FROM_INVALID_STATE |
| Issue a corrective invoice against it | 422 PROFORMA_CORRECTIVE_FORBIDDEN |
Change its type on update | 422 PROFORMA_TYPE_CHANGE_FORBIDDEN |
| Use it as the source of a recurring invoice | 422 PROFORMA_NOT_RECURRING |
To withdraw an offer, void it with
POST …/invoices/{invoice_id}/void. For a proforma this is
a plain status change: no corrective invoice, nothing sent to the AEAT. The reason field
is still required (10 to 500 characters). A voided proforma stays on record: it cannot be
deleted (400 STATUS_NOT_DELETABLE), and voiding it again answers 409 INVOICE_ALREADY_VOIDED.
Convert it into an invoice
When the customer accepts, call
POST /v1/companies/{company_id}/invoices/{invoice_id}/convert-to-invoice.
BeeL. creates a new STANDARD invoice from the proforma's recipient, lines, payment
information and notes. The proforma itself is not modified: it keeps its PRO-... number
and PDF as the record of what the customer accepted, and moves to CONVERTED.
The body is optional and has a single field, issue (false when omitted). Without a body,
still send Content-Type: application/json: a request with no content type answers
415 UNSUPPORTED_MEDIA_TYPE.
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/convert-to-invoice" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Idempotency-Key: convert-quote-2026-0042" \
-H "Content-Type: application/json" \
-d '{ "issue": false }'The new invoice is a DRAFT with no number, in the company's default STANDARD series.
Review or edit it, then issue it through the normal flow.
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/convert-to-invoice" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Idempotency-Key: convert-quote-2026-0042" \
-H "Content-Type: application/json" \
-d '{ "issue": true }'The new invoice is numbered and issued in the same call. The operation is atomic:
if issuing fails, nothing is created and the proforma stays ACTIVE, so you can fix the
cause and call again.
Either way the answer is 201 with the new invoice in data. It carries
source_proforma_id, pointing back at the proforma. In the other direction, the detail of
a CONVERTED proforma (GET …/invoices/{invoice_id})
carries converted_invoice_id. List rows do not include it.
Conversion rules
- Only
ACTIVEconverts. A proforma shown asEXPIREDis stillACTIVEunderneath and converts too. A voided one answers422PROFORMA_NOT_CONVERTIBLE. - It converts once. A second call, with
issuetrueorfalse, answers409PROFORMA_ALREADY_CONVERTEDand never creates a second invoice. Retrying after a timeout is safe. - A converted proforma is frozen. Editing it answers
422STATUS_NOT_MODIFIABLE, deleting it400STATUS_NOT_DELETABLEand voiding it400TRANSITION_NOT_SUPPORTED. - Only proformas convert. Any other document answers
422CONVERSION_REQUIRES_PROFORMA. - Issuing needs a ready company. With
issue: true, a company that cannot issue yet in this environment answers422EMISSION_NOT_READY, with the reasons inerror.details.blockers[].
Undoing a conversion
If the conversion produced a draft and you delete that draft, the proforma goes back
from CONVERTED to ACTIVE, editable and convertible again. This is the only way back.
Voiding or correcting an invoice that was already issued does not return its proforma.
Proformas in lists and totals
Proformas appear in the normal invoice list alongside fiscal documents. Filter
GET …/invoices with:
type=PROFORMAto see only proformas;fiscal_only=trueto see only fiscal documents (STANDARD,CORRECTIVE,SIMPLIFIED). It is ignored when you also sendtype.
Figures that describe your invoicing leave proformas out: the
fiscal summary only adds up fiscal invoices, and a company's
invoice_count does not count proformas.
Stripe
If you connected Stripe, "proforma" also comes up on the Stripe side: you mark Stripe's
own invoices as proforma so that the only fiscal invoice your customer receives is the one
BeeL. issues. That is a Stripe setting, unrelated to the PROFORMA type described here.
See Configure proforma invoices in Stripe.
Related
Recurring invoices
Schedule invoices that generate themselves — cadence, first occurrence, review drafts, automatic sending, how a series ends, and what happens when a generation fails.
Branding and PDFs
Set the logo, template, colour and languages of a company's invoices, preview a draft, and download the final PDF without writing a polling loop.