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 |
| Change template, colour or languages | PUT …/invoice-customization |
| Upload or replace the logo | PUT …/logo |
| Remove the logo | DELETE …/logo |
| List the templates with readable names | GET /v1/invoice-customization-options |
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.
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"{
"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 or
422 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 |
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. 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:
{
"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
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.
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.
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.
Preview a draft
A draft has no fiscal PDF yet. To see what it will look like, render it on the fly:
curl https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf/preview \
-H "Authorization: Bearer $BEEL_API_KEY" \
-o draft.pdfGET …/pdf/preview 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 here: it already has a stored PDF, which
you download with the next operation.
There is also GET …/preview, 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.
Download the final PDF
GET …/pdf 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.
curl https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf \
-H "Authorization: Bearer $BEEL_API_KEY"{
"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.
202is the exception. It means the wait ran out with the PDF still in progress. There is no body. Ask again after the seconds given inRetry-After.Prefer: wait=Nlimits the wait toNseconds.Prefer: wait=0never waits: you get an immediate202while the PDF is in progress. A value above 10 is lowered to 10, and the202reports the wait actually used inPreference-Applied. A value that is not a non-negative integer is ignored and the default wait applies.- Drafts and scheduled invoices answer
400INVOICE_NOT_ISSUED_NO_PDFstraight away, without waiting: they will never have a fiscal PDF until they are issued. Use…/pdf/previewfor them. - An invoice under VeriFactu that is not registered with the AEAT answers
400INVOICE_NOT_REGISTERED_NO_PDFstraight 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_messagesays why.
# 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 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.
{
"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:
- Sending the email yourself. Nothing is emailed unless you ask for it — see Turning off BeeL.'s email.
- Rendering the invoice document yourself, with BeeL.'s
invoice_numberand the QR data the API returns.
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, and what applies to the QR in Rendering your own PDF.
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-customizationdoes 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/previewshows the current branding and changes nothing; issued invoices keep the PDF they were delivered with. - A
202from…/pdfis not an error. HonourRetry-Afterand ask again.