# 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.

Every company (NIF) carries its own branding: a logo, a PDF template, an accent colour, the
language the invoice is printed in and the language of the emails that deliver it. There is
no account-wide branding. Two companies of the same account look different if you set them
up differently.

| What | Operation |
|---|---|
| Read the current branding | [`GET …/invoice-customization`](/companies/getCompanyInvoiceCustomization) |
| Change template, colour or languages | [`PUT …/invoice-customization`](/companies/updateCompanyInvoiceCustomization) |
| Upload or replace the logo | [`PUT …/logo`](/companies/uploadCompanyLogoById) |
| Remove the logo | [`DELETE …/logo`](/companies/deleteCompanyLogoById) |
| List the templates with readable names | [`GET /v1/invoice-customization-options`](/invoice-customization/listInvoiceCustomizationOptions) |

## Upload the logo

The logo goes as `multipart/form-data` in a field named `file`. It must be **JPEG or PNG, up
to 1 MB**. BeeL. resizes it to fit within 300×300 pixels, keeping its aspect ratio, stores it
as a JPEG and replaces any previous logo. A 600×300 PNG comes back as a 300×150 JPEG.

```bash
curl -X PUT https://app.beel.es/api/v1/companies/{company_id}/logo \
  -H "Authorization: Bearer $BEEL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@logo.png"
```

```json
{
  "success": true,
  "data": {
    "logo_url": "https://storage.example.com/beel-public/logos/9a1f.../profile-logo.jpg?v=1790235789896"
  },
  "meta": { "timestamp": "2026-09-24T10:00:00Z", "request_id": "4bf92f3577b34da6a3ce929d0e0e4736" }
}
```

A file that is too big or of the wrong type answers `422` [`FILE_TOO_LARGE`](/errors/FILE_TOO_LARGE) or
`422` [`INVALID_FILE_TYPE`](/errors/INVALID_FILE_TYPE) in `error.code`. The type is checked on the file itself, not its name: a
GIF is refused, and so is a PNG uploaded as `logo.txt`. `DELETE …/logo` answers `204`. It also answers `204`
when there was no logo to delete.

## Choose template, colour and languages

`PUT …/invoice-customization` is a **partial update**: only the properties you send change.
The logo is not part of this body. It has its own sub-resource, shown above.

| Property | Values |
|---|---|
| `invoice_template_type` | `MODERN_TABLE` (a table of product/service lines) or `PROFESSIONAL_SERVICE` (a text-based layout) |
| `invoice_accent_color` | `#RRGGBB`, upper or lower case, kept as sent. The short form `#RGB` is refused. |
| `invoice_language` | `es`, `en` or `ca`: the language the PDF is printed in |
| `email_language` | `es`, `en` or `ca`: the language of the emails that deliver the invoice |

```bash
curl -X PUT https://app.beel.es/api/v1/companies/{company_id}/invoice-customization \
  -H "Authorization: Bearer $BEEL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "invoice_template_type": "PROFESSIONAL_SERVICE",
    "invoice_accent_color": "#1d4ed8",
    "invoice_language": "en"
  }'
```

An invalid value answers `422` [`VALIDATION_ERROR`](/errors/VALIDATION_ERROR). A property the body does not know, such as
`logo_url`, is ignored. The response is the full customization, with `email_language` and
`logo_url` unchanged:

```json
{
  "success": true,
  "data": {
    "company_id": "9a1f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "invoice_template_type": "PROFESSIONAL_SERVICE",
    "invoice_accent_color": "#1d4ed8",
    "invoice_language": "en",
    "email_language": "es",
    "logo_url": "https://storage.example.com/beel-public/logos/9a1f.../profile-logo.jpg?v=1790235789896"
  },
  "meta": { "timestamp": "2026-09-24T10:01:00Z", "request_id": "5c0a4e6f1d2b4c8e9f7a6b5c4d3e2f1a" }
}
```

### Showing the templates to a person

The accepted template codes are the `invoice_template_type` enum. If you build a picker,
[`GET /v1/invoice-customization-options`](/invoice-customization/listInvoiceCustomizationOptions)
gives each `code` a translated `name` and `description`, so you don't have to show
`MODERN_TABLE` to a person. It is a closed catalogue: it is the same for every account,
it takes no `company_id` and it is not paginated.

> **Rules that apply here:** [CNT-017 · An invoice may be in any language](/rules/contents#cnt-017)

## Branding and invoices you already issued

The PDF of an issued invoice is generated **once**, and under VeriFactu once the QR of its
registration exists. It is never modified afterwards: a change of branding, of template or of
BeeL.'s layout (such as where the VeriFactu QR is printed) applies to the invoices issued after
it, and a customer who downloads an invoice issued last month keeps getting the document as it
was delivered.

<Callout type="info">
  Voiding an invoice or issuing a corrective for it, including a `TOTAL` corrective, does not
  change its PDF and does not send `invoice.pdf.generated`. The invoice's status is in the API
  (`status`, and `verifactu.submission_status` for its registration) and in the app, not in the
  document. In the sandbox every PDF carries the test-invoice watermark, as part of the document
  issued there.
</Callout>

## Preview a draft

A draft has no fiscal PDF yet. To see what it will look like, render it on the fly:

```bash
curl https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf/preview \
  -H "Authorization: Bearer $BEEL_API_KEY" \
  -o draft.pdf
```

[`GET …/pdf/preview`](/invoices/previewCompanyInvoicePdf) answers the PDF itself
(`application/pdf`), not a URL. Nothing is stored and no number is used up, so every call
shows the latest version of the draft with the branding set at that moment. That makes
it the way to check a new template or colour before you issue anything. The document is
marked as a draft in the invoice language (`BORRADOR` in Spanish, `DRAFT` in English) and has
no VeriFactu QR code. A scheduled invoice can be previewed the same way.

An issued invoice answers `400` [`PREVIEW_DRAFT_ONLY`](/errors/PREVIEW_DRAFT_ONLY) here: it already has a stored PDF, which
you download with the next operation.

<Callout type="info">
  There is also [`GET …/preview`](/invoices/getCompanyInvoicePreview), which returns a short-lived
  URL to a WebP **image** of the invoice for showing inline, for example as a thumbnail in a list.
  It is not a PDF.
</Callout>

## Download the final PDF

[`GET …/pdf`](/invoices/getCompanyInvoicePdf) returns a pre-signed download URL. The URL expires
in **five minutes** and only allows `GET`. Ask for a new one each time you need it instead of
storing it.

```bash
curl https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf \
  -H "Authorization: Bearer $BEEL_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "download_url": "https://storage.example.com/beel-invoices/...&X-Amz-Signature=...",
    "expires_in_seconds": 300,
    "file_name": "factura_2026-0042.pdf"
  },
  "meta": { "timestamp": "2026-09-24T10:05:00Z", "request_id": "7d1e2f3a4b5c4d6e8f9a0b1c2d3e4f5a" }
}
```

### The request waits for the PDF

A PDF is produced asynchronously after issuing. You don't need a polling loop: if the PDF is
still being generated, the request **waits for it, up to 10 seconds**, and then answers `200`.

- **`202`** is the exception. It means the wait ran out with the PDF still in progress. There
  is no body. Ask again after the seconds given in `Retry-After`.
- **`Prefer: wait=N`** limits the wait to `N` seconds. `Prefer: wait=0` never waits: you get
  an immediate `202` while the PDF is in progress. A value above 10 is lowered to 10, and the
  `202` reports the wait actually used in `Preference-Applied`. A value that is not a
  non-negative integer is ignored and the default wait applies.
- **Drafts and scheduled invoices** answer `400` [`INVOICE_NOT_ISSUED_NO_PDF`](/errors/INVOICE_NOT_ISSUED_NO_PDF) straight away,
  without waiting: they will never have a fiscal PDF until they are issued. Use `…/pdf/preview`
  for them.
- **An invoice under VeriFactu that is not registered with the AEAT** answers
  `400` [`INVOICE_NOT_REGISTERED_NO_PDF`](/errors/INVOICE_NOT_REGISTERED_NO_PDF) straight away: its PDF would carry the
  QR of a registration that does not exist, because it was rejected before reaching the AEAT or the
  invoice was voided without being registered. `verifactu.error_message` says why.

```bash
# Don't wait more than 3 seconds
curl https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf \
  -H "Authorization: Bearer $BEEL_API_KEY" \
  -H "Prefer: wait=3"
```

## Know when a PDF changes

The [`invoice.pdf.generated`](/webhook-events/onInvoicePdfGenerated) webhook is sent when BeeL. stores an
invoice's PDF. For an issued invoice that is **once**, when its PDF is generated. A proforma sends it
each time its PDF is generated: when it is created and after each edit. In an exceptional case BeeL.
staff may regenerate the PDF of an issued invoice to fix a rendering defect; that sends the event
again, and never emails the customer.

`data.generation` numbers the stored PDF of that invoice: `1` for the first one. Use it to discard
an older generation that arrives late. A redelivery of the same generation is a retry, with the same
`BeeL-Event-Id`. Events recorded before the field existed do not carry it: treat a missing value as
unknown, not as `1`.

```json
{
  "id": "5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f",
  "type": "invoice.pdf.generated",
  "created_at": "2026-09-24T10:04:58Z",
  "api_version": "2025-01",
  "livemode": true,
  "data": {
    "invoice_id": "550e8400-e29b-41d4-a716-446655440000",
    "invoice_number": "2026/0042",
    "generation": 1
  }
}
```

The payload has **no URL**, on purpose. A download URL lasts five minutes and a webhook can
be retried for longer than that. When the event arrives, call `GET …/pdf` for a fresh URL
and drop any copy you had. `invoice_number` is `null` for a proforma, which has no number yet.

## White-label: invoices without BeeL. in sight

The API can stay invisible to the business you invoice for and to its customers, but BeeL.'s own PDF and email are not white-label. What shows BeeL. and what does not:

| Piece | Shows BeeL.? |
|---|---|
| The API itself, its responses and webhooks | Only to you, the integrator |
| The QR and its `qr_url` | No — the link points to AEAT's validation service, with the issuer's NIF |
| BeeL.'s PDF | Yes: a footer reading "Generated with BeeL." (in the invoice's language), with a link. It cannot be turned off; the logo, template and colour are the company's |
| BeeL.'s invoice email | Yes: it is sent from a BeeL. address and ends with "Sent with BeeL.". The sender name is the company's trade name, and replies go to the company's email |

Keeping BeeL. out of sight therefore means two things:

1. **Sending the email yourself.** Nothing is emailed unless you ask for it — see [Turning off BeeL.'s email](/guides/erp-integration#turning-off-beels-email).
2. **Rendering the invoice document yourself**, with BeeL.'s `invoice_number` and the QR data the API returns.

<Callout type="warn" title="Rendering the invoice yourself is a compliance decision, not only a design one">
  Under the criteria AEAT has published, software that prints the invoice or generates its QR can be a component of the billing system that needs its own *declaración responsable* (the producer's signed statement of compliance). BeeL.'s declaration covers BeeL., not the document your software renders. Check this architecture with your tax advisor before you build it — the rules are quoted in [Software built on the API](/verifactu/compliance-and-responsibilities#software-built-on-the-api), and what applies to the QR in [Rendering your own PDF](/verifactu/qr-and-pdf#rendering-your-own-pdf).
</Callout>

Your own document does not change what is registered with AEAT: the record is the one BeeL. generated, with BeeL.'s number.

## Gotchas

- **Branding belongs to the company.** Setting it for one NIF doesn't set it for the others
  in the account.
- **`PUT …/invoice-customization` does not touch the logo.** Upload and delete it through
  `…/logo`.
- **Download URLs expire in five minutes.** Don't store them or send them in an email.
  Ask for a fresh one.
- **Check a new template on a draft.** `…/pdf/preview` shows the current branding and changes
  nothing; issued invoices keep the PDF they were delivered with.
- **A `202` from `…/pdf` is not an error.** Honour `Retry-After` and ask again.

## Related

<Related>

- [Get the invoice customization](/companies/getCompanyInvoiceCustomization) ·
  [Update it](/companies/updateCompanyInvoiceCustomization)
- [Upload the logo](/companies/uploadCompanyLogoById) ·
  [Delete the logo](/companies/deleteCompanyLogoById)
- [List the invoice templates](/invoice-customization/listInvoiceCustomizationOptions)
- [Preview a draft PDF](/invoices/previewCompanyInvoicePdf) ·
  [Download the PDF](/invoices/getCompanyInvoicePdf) ·
  [Preview image](/invoices/getCompanyInvoicePreview)

</Related>

---

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