# 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`](/errors/VALIDATION_ERROR) (`error.details.recipient`), and an incomplete ad-hoc
recipient `422` [`RECIPIENT_ID_REQUIRED`](/errors/RECIPIENT_ID_REQUIRED), [`RECIPIENT_FISCAL_NAME_REQUIRED`](/errors/RECIPIENT_FISCAL_NAME_REQUIRED) or
[`RECIPIENT_ADDRESS_REQUIRED`](/errors/RECIPIENT_ADDRESS_REQUIRED).

```bash
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`](/errors/PROFORMA_NOT_ISSUABLE) |
| Schedule it for later issuance | `400` [`PROFORMA_SCHEDULE_FORBIDDEN`](/errors/PROFORMA_SCHEDULE_FORBIDDEN) |
| Mark it as paid | `400` [`STATUS_NOT_MODIFIABLE`](/errors/STATUS_NOT_MODIFIABLE) |
| Mark it as sent | `400` [`MARK_SENT_FROM_INVALID_STATE`](/errors/MARK_SENT_FROM_INVALID_STATE) |
| Issue a corrective invoice against it | `422` [`PROFORMA_CORRECTIVE_FORBIDDEN`](/errors/PROFORMA_CORRECTIVE_FORBIDDEN) |
| Change its `type` on update | `422` [`PROFORMA_TYPE_CHANGE_FORBIDDEN`](/errors/PROFORMA_TYPE_CHANGE_FORBIDDEN) |
| Use it as the source of a recurring invoice | `422` [`PROFORMA_NOT_RECURRING`](/errors/PROFORMA_NOT_RECURRING) |

To withdraw an offer, void it with
[`POST …/invoices/{invoice_id}/void`](/invoices/voidCompanyInvoice). 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`](/errors/STATUS_NOT_DELETABLE)), and voiding it again answers `409` [`INVOICE_ALREADY_VOIDED`](/errors/INVOICE_ALREADY_VOIDED).

## Convert it into an invoice

When the customer accepts, call
[`POST /v1/companies/{company_id}/invoices/{invoice_id}/convert-to-invoice`](/proforma/convertCompanyProformaToInvoice).
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`](/errors/UNSUPPORTED_MEDIA_TYPE).

<Tabs items={['Leave it as a draft', 'Issue it in the same call']}>
  <Tab value="Leave it as a draft">
    ```bash
    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.
  </Tab>
  <Tab value="Issue it in the same call">
    ```bash
    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.
  </Tab>
</Tabs>

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}`](/invoices/getCompanyInvoice))
carries `converted_invoice_id`. List rows do not include it.

### Conversion rules

- **Only `ACTIVE` converts.** A proforma shown as `EXPIRED` is still `ACTIVE` underneath and
  converts too. A voided one answers `422` [`PROFORMA_NOT_CONVERTIBLE`](/errors/PROFORMA_NOT_CONVERTIBLE).
- **It converts once.** A second call, with `issue` `true` or `false`, answers
  `409` [`PROFORMA_ALREADY_CONVERTED`](/errors/PROFORMA_ALREADY_CONVERTED) and never creates a second invoice. Retrying after a
  timeout is safe.
- **A converted proforma is frozen.** Editing it answers `422` [`STATUS_NOT_MODIFIABLE`](/errors/STATUS_NOT_MODIFIABLE),
  deleting it `400` [`STATUS_NOT_DELETABLE`](/errors/STATUS_NOT_DELETABLE) and voiding it `400` [`TRANSITION_NOT_SUPPORTED`](/errors/TRANSITION_NOT_SUPPORTED).
- **Only proformas convert.** Any other document answers `422` [`CONVERSION_REQUIRES_PROFORMA`](/errors/CONVERSION_REQUIRES_PROFORMA).
- **Issuing needs a ready company.** With `issue: true`, a company that cannot issue yet in
  this environment answers `422` [`EMISSION_NOT_READY`](/errors/EMISSION_NOT_READY), with the reasons in
  `error.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`](/invoices/listCompanyInvoices) with:

- `type=PROFORMA` to see only proformas;
- `fiscal_only=true` to see only fiscal documents (`STANDARD`, `CORRECTIVE`, `SIMPLIFIED`).
  It is ignored when you also send `type`.

Figures that describe your invoicing leave proformas out: the
[fiscal summary](/guides/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](/stripe/configuracion-proforma).

> **Rules that apply here:** [LIF-003 · Test in the sandbox, never with real invoices](/rules/lifecycle#lif-003)

## Related

<Related>

- [Invoice lifecycle](/guides/invoice-lifecycle) — the statuses the converted invoice goes through
- [Configure proforma invoices in Stripe](/stripe/configuracion-proforma) — keep Stripe invoices as proformas
- [Fiscal summary](/guides/fiscal-summary) — the totals proformas stay out of

</Related>

---

Full OpenAPI spec: https://docs.beel.es/api/openapi