---
title: "Get the PDF download URL of an invoice"
method: GET
path: "/v1/companies/{company_id}/invoices/{invoice_id}/pdf"
tags: ["CompanyInvoiceDelivery"]
---

# Get the PDF download URL of an invoice

`GET /v1/companies/{company_id}/invoices/{invoice_id}/pdf`

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/pdf`
- Auth: HTTP bearer

Returns a temporary pre-signed URL to download the invoice PDF.

- **URL:** expires in five minutes and only allows `GET`.
- **Waiting:** a PDF is produced asynchronously, so this request **waits** for it (up to
  ten seconds) instead of handing you a polling loop to write. Bound the wait with
  `Prefer: wait=N`, or opt out with `Prefer: wait=0`.
- **`202`:** only when the wait elapsed with the PDF still in flight. No body is returned;
  ask again after `Retry-After`.
- **Drafts:** a draft has no fiscal PDF and answers `400 INVOICE_NOT_ISSUED_NO_PDF`
  immediately — that one never waits. Issue it, or render it with
  `GET …/{invoice_id}/pdf/preview`.
- **Not registered with the AEAT:** under VeriFactu the PDF carries the QR code of the
  invoice's registration. An invoice whose registration was rejected before reaching the
  AEAT, or that was voided without ever being registered, has no PDF and answers
  `400 INVOICE_NOT_REGISTERED_NO_PDF` immediately. Its `verifactu.error_message` says why.
- **Never modified:** the PDF of an issued invoice is generated once — with its VeriFactu QR
  when it applies — and stays the document you delivered. Voiding the invoice or issuing
  a corrective against it does not change the PDF: read the invoice's `status` to know it.

## Headers

- `Prefer` string

## Response `200`

Download URL generated successfully

- InvoicePdfResponse
  - `success` boolean, required
  - `data` object, required
    - `download_url` string, uri, required: Pre-signed URL to download the PDF (valid for 5 minutes)
    - `expires_in_seconds` integer, required: Seconds until the URL expires
    - `file_name` string, required: Suggested filename for download
  - `meta` ResponseMeta, required
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `202`: The PDF was still not ready when the wait window elapsed, so no body is returned. Uncommon: the request waits for the PDF by default and normally answers `200`. It is what you always get with `Prefer: wait=0`. Ask again — `Retry-After` says how soon.
- `400`: `INVOICE_NOT_ISSUED_NO_PDF` — the invoice is a draft: it has no fiscal PDF and no emission is generating one. Issue it, or use `…/pdf/preview`. `INVOICE_NOT_REGISTERED_NO_PDF` — the invoice is not registered with the AEAT, so there is no QR code to print and no PDF: its registration was rejected before reaching the AEAT, or it was voided without being registered. Answered immediately.
- `401`: Missing or invalid authentication. Like every other error, `message`/`detail` is localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the header is missing or asks for none of those.
- `403`: Your account does not own or manage this company, or does not hold the required access over it. A NIF that does not exist answers the same way. That case answers `error.code` `ACTIVE_COMPANY_NOT_ACCESSIBLE`.
- `404`: The resource addressed by the path does not exist, or is not one this credential can see — the two are answered identically, so existence is never disclosed. `error.code` names the kind of resource that was missing, so a client can tell which of several ids in a path failed to resolve: - `INVOICE_NOT_FOUND` — the `{invoice_id}` (invoices, proformas and their sub-resources). - `RECURRING_NOT_FOUND` — the `{recurring_invoice_id}`. - `CLIENT_NOT_FOUND` — the `{customer_id}`. - `INVOICE_CLIENT_NOT_FOUND` — the `customer_id` referenced by an invoice body does not resolve to a customer of the issuing company. - `PRODUCT_NOT_FOUND` — the `{product_id}`. - `SERIES_NOT_FOUND` — the `{series_id}`. - `WEBHOOK_SUBSCRIPTION_NOT_FOUND`, `DELIVERY_LOG_NOT_FOUND` — the `{webhook_id}` and the `{delivery_id}`. - `PROFILE_NOT_FOUND` — the credential's account has no company profile yet: its holder has not completed onboarding, so there is no issuer to act as. - `NOT_FOUND` — the generic fallback, for the few resources that carry no code of their own. Some resources declare a more specific `404` response of their own (`CONNECTION_NOT_FOUND`, `MEMBER_NOT_FOUND`, `GRANT_NOT_FOUND`, `INVITATION_NOT_FOUND`, `REQUEST_LOG_NOT_FOUND`); it is documented on the operation. A `404` with `ENDPOINT_NOT_FOUND` is a different answer: the **path itself** does not exist in this API (a typo in the route, or a resource that was never here). It says nothing about any resource. When the path exists but not with that method, the answer is `405`, not `404`.
- `429`: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because too many went out to that recipient in a short time. Either way, retry after the `Retry-After` seconds. `QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan for the current period (issued invoices, for instance). Retrying does not help until the quota resets or the plan changes. `error.details` carries `feature_code`, `current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until the reset when it is known.
- `500`: Internal server error
- `default`: Any status code the operation does not list above. Every operation declares it, so a generated client always has a branch to fall into and never loses the cause of a failure it did not anticipate. This is where the transport-level answers land — `405`, `406`, `415` and `429` — together with any status a future version of the API starts returning. All of them carry the same error envelope as the codes listed explicitly, so `error.code` is what tells them apart: switching on the status code alone is not enough. See «Transport-level errors» in the API description for when each one is produced. A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active subscription. `error.details` says why and what to do: `current_status` (absent if it never had one), `required_permission` and `action`. A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a dependency of BeeL itself (the database, the email provider) was momentarily unavailable and nothing was done. Retry after those seconds. A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the operation depends on failed or did not answer in time. It is a transient condition — retry with the same `Idempotency-Key` where the operation accepts one. One exception to the envelope: a failure of the network edge that never reaches the application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not BeeL's — it may not even be JSON. Treat those as "no answer", and retry.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
