Sending email
How BeeL. delivers invoice emails — the sandbox recipient restriction, per-account send quotas, queued delivery, and bulk limits.
Sending an invoice by email is POST /v1/companies/{company_id}/invoices/{invoice_id}/send.
Two things about it are not obvious from the endpoint reference, and both will bite an
integration: the sandbox only lets you write to yourself, and there are send quotas
that are separate from the API rate limits.
Sandbox only sends to your own address
With a beel_sk_test_ key you can only send to the account holder's own address.
Any other recipient is refused with 403 ENVIO_NO_PERMITIDO.
Plus-addressing is fully supported, and it is the intended way to separate test
sends. The comparison strips the +tag before matching, so all of these count as your
address:
you@yourdomain.com
you+test1@yourdomain.com
you+customer-fixture@yourdomain.comThe rule applies to every recipient, to and cc.
In production this rule does not apply: you send to whoever the invoice is addressed to.
The 403 is specific to test keys.
Send quotas
Separate from the API rate limits, which count HTTP requests, there are quotas on emails actually sent, per account. Note the unit: one bulk call with 40 recipients is one request but forty emails.
| Axis | Production | Sandbox |
|---|---|---|
| Emails per hour | 60 | 10 |
| Emails per 24 h | 300 | 30 |
| Distinct recipient addresses per 24 h | 100 | 5 |
| Sends of the same invoice per 24 h | 10 | 10 |
The windows start at your first send, not on the clock. Each one opens with your first send and lasts 24 hours (or one hour) from there — there is no reset at midnight. Send at 23:00 and resume at 09:00 and you are still inside the same window.
The distinct-recipients axis counts how many different addresses you wrote to, not how
many emails you sent: ten invoices to the same customer count as one address. On this axis
a +tag makes a different address: you+test1@… and you+test2@… are two of the sandbox's
5. Each environment counts its own addresses: those you
write to in the sandbox do not use up the production allowance, nor the other way round.
Every email your account sends counts, not just invoices. Account verification, password resets, invitations and welcome messages come out of the same allowance, because they are attributed to your account too. Only mail with no account behind it at all is exempt. In practice this is a handful a month, but it is the same bucket.
Spam complaints reduce these ceilings — each complaint in the last 30 days halves them, and at 3 the account is suspended from sending. Ordinary invoicing never approaches this.
Need more? These are defaults, not a hard ceiling. If your billing volume needs higher limits, ask us at it@beel.es and tell us your monthly volume and your peak day — the link opens a message with the fields we need.
What you get back
| Status | When |
|---|---|
429 | Quota exhausted on some axis. There is a Retry-After — it will work later. |
403 | Not a quota problem: the destination itself is not allowed. Waiting changes nothing. |
503 | The quota could not be checked right now. There is a Retry-After; the same send works once it passes. |
The response does not name which axis you hit or how much is left, so program against the status code rather than trying to read a reason out of the body.
Where the rejection reaches you
Both checks run synchronously when you ask for the send — /send and
/deliveries. There you get the 429 or 403 as the response, and nothing was queued.
An auto-sent invoice is different. When you create an invoice with
send_automatically, the creation succeeds and returns 201; the email is authorised
later, when it is delivered. So a quota or destination rejection does not reach you
as an error on that call — it lands in the delivery history as a failed send. If you
rely on automatic sending, read the outcome there rather than from the creation
response.
Sending a whole month at once
Plenty of integrations bill on the last day of the month and send everything in one run. That is the run the quotas bind on first, so it is worth pacing rather than firing in a loop.
Work out which limit you hit first. For a run of N invoices to N different customers, in production:
| Customers in the run | What stops you |
|---|---|
| Up to 60 | Nothing, if you pace it across the hour |
| 60–100 | The hourly cap — spread the run out |
| Over 100 | The distinct-recipients cap — the run does not fit in one window |
The 300-email cap is rarely the binding one. Distinct recipients (100 per 24 h) is, because a month-end run is by definition one email each to many different people.
How to run it
- Pace the run evenly across the hour. Staying under 60 emails an hour means you never
see a
429. - Handle the
429as a pause, not a failure. It carriesRetry-Afterin seconds — sleep that long and continue from the same invoice. Nothing was sent, so there is nothing to undo. - Split a run of more than 100 customers, and mind where the window starts. The
24 hours run from your first send, not from midnight: if you send 100 at 23:00, you are
not clear again until 23:00 the next day, not at 00:00. Plan the batches from your own
start time. Issuing the invoices is not limited — only sending them is, so you can
create all 250 on the last day and drain the sending queue over the following days.
The
429carriesRetry-Afterwith the exact seconds left, so the safest loop is to honour it rather than compute the window yourself. - Ask for more instead of engineering around it if this is your normal shape. Tell us your peak day using the link above and we will raise the limits.
Where bulk delivery helps, and where it does not
POST /v1/companies/{company_id}/invoices/deliveries puts many invoices in one email,
so it costs one email against your quota no matter how many invoices it carries.
That helps when several invoices go to the same recipient — a customer with twelve monthly invoices, or a copy of everything to your own accountant. It does not help a month-end run to many different customers: each customer still needs their own email, so you still spend one email and one distinct recipient per customer.
Delivery itself is asynchronous
Once the send is authorised, the actual delivery is queued. A 2xx therefore means
accepted for delivery, not delivered:
sent_atin the response is when BeeL. queued the email. On the invoice resource,sent_atis when the provider accepted the message — still not when it reached the mailbox.email_idin the/sendresponse is not the provider's message id.- A send can still fail after the
2xx— a bounce or a provider outage surfaces later.
Sending before the PDF exists
The PDF of an issued invoice is generated after issuing, and under VeriFactu only once the QR of
its registration exists. A /send with attach_pdf: true (the default) that arrives before
then is not refused: it answers 202. The email goes out as soon as the PDF is stored, with
it attached, the same way as the automatic email.
sent_atin the202is when the request was accepted, not when the email went out: it has not gone out yet. Follow it withemail_idin the delivery history or its webhook for the real send time.- If the invoice never gets a PDF — a VeriFactu registration that ends without a QR — the email is recorded as failed, with its reason. It is not sent without the PDF.
- An invoice that is already known to have no PDF — under VeriFactu and not registered with
the AEAT — answers
400INVOICE_NOT_REGISTERED_NO_PDFinstead of202.verifactu.error_messagesays why.
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/invoices/{invoice_id}/send" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recipients": ["you+test@yourcompany.com"] }'
# → 202 while the PDF is being generated; 200 once it existsObserving what actually happened
| Source | What it gives you |
|---|---|
GET /v1/accounts/{account_id}/emails | Delivery history: one record per email with status, recipients and timestamps |
GET /v1/accounts/{account_id}/email-indicators | Aggregate counters for the account |
The invoice's sending_history | Every send of that invoice — sending it again appends, it never overwrites. Only single-invoice responses carry it; the list endpoint omits it. A failed or rejected send carries error: a short explanation in the language of the request, for a person to read — branch on status, never on its text |
The invoice.email.sent webhook | Fires on a successful send — see Events |
Statuses are QUEUED, REJECTED, SENT, FAILED, DELIVERED, BOUNCED and OPENED. A
message stopped by policy is recorded as REJECTED and never sent — it is listed rather
than omitted, and it is terminal, so nothing retries it.
Retries
A failed delivery is retried with exponential backoff and jitter, up to 5 attempts, and then given up on — nothing retries it for you after that, so the delivery record is the only place the outcome lives. Failures that cannot improve on a retry (an invalid recipient, a rejected destination) are given up on the first attempt instead of burning the whole budget.
Recipients
The recipient list is resolved in this order, first match wins:
recipientsin the/sendbody- The invoice's own
email_config - The customer's
billing_emails - The customer's
email
billing_emails are the addresses the customer wants its invoices at — accounts payable, a
shared invoices inbox — so they take precedence over email, which is the general contact
address and only receives the invoice when no billing address is set. An invoice generated by
a recurring invoice resolves the same way, with the template's
email_configuration.recipients in the place of the invoice's email_config.
The same order applies to the manual /send, to the automatic send when an invoice is issued with send_automatically: true, and to recurring invoices. An invoice always keeps its customer — duplicating it, correcting it, converting a proforma or creating a recurring invoice from it all preserve customer_id — so a corrective issued with send_automatically: true from an invoice that has a customer uses the customer's addresses too, with no ad-hoc recipient needed.
If nothing resolves, the call fails with 422 INVOICE_EMAIL_NO_RECIPIENTS.
Only an issued invoice is sent: a draft or a scheduled invoice answers
400 INVOICE_DRAFT_NOT_SENDABLE — issue it first. An invoice whose PDF does not exist yet is accepted with 202, see
Sending before the PDF exists.
Reserved domains never send
Addresses on the reserved documentation domains and non-routable TLDs of
RFC 2606 and
RFC 6761 can never receive mail. When none of
an email's recipients can, nothing is sent: /send answers 403 ENVIO_NO_PERMITIDO, and an
automatic send is recorded as REJECTED. When at least one recipient is real, the email goes
out and the reserved addresses simply bounce.
| Rejected | Values |
|---|---|
| Domains | example.com, example.net, example.org (and any subdomain) |
| TLDs | .test, .example, .invalid, .localhost, .local |
A fixture customer whose only address is cliente@example.com never receives anything.
Use a +tag on your own address instead — which is what the sandbox requires anyway.
Bulk sending
POST /v1/companies/{company_id}/invoices/deliveries sends the PDFs of several invoices
as one email to the recipients you name — never one email per customer. Its limits,
attachments and per-invoice failures are in
Bulk operations and exports;
what it costs against your quota is above.
Checklist
- In sandbox, send only to your own address — use
+tagto separate test runs - Treat
403ENVIO_NO_PERMITIDOas "change the recipient", never as "retry" - Treat
429on/sendas a send quota, not an API rate limit - Treat a
2xxas "queued", and read the delivery history for the real outcome - Treat a
202from/sendas "will go out when the PDF is stored", not as sent - Never put
example.comor a.testaddress in fixtures - Pace month-end runs under the hourly cap, and split them if they exceed 100 customers
- Remember the windows roll from your first send, not from midnight
- Treat an auto-sent invoice's
201as "created", not as "email accepted" - Use bulk delivery when several invoices share a recipient — not as a way around the caps
- Write to it@beel.es if your volume needs higher limits
Related
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.
Bulk operations and exports
Issue, mark, download, email and export many invoices at once, and create or delete customers and products in batches — and how to read what came back.