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 | 100 |
| Sends of the same invoice per 24 h | 10 | 10 |
The windows are rolling, not calendar. Each one starts at 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.
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 three 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. |
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 to roughly one email per minute. That keeps you under 60/hour without ever
seeing 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.
Observing 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 — a resend appends, it never overwrites |
The invoice.email.sent webhook | Fires on a successful send — see Events |
Statuses are SENT, FAILED, DELIVERED, BOUNCED and OPENED.
Retries
A failed delivery is retried with exponential backoff and jitter, up to 3 attempts, then dead-lettered — nothing retries it for you after that. Failures that cannot improve on a retry (an invalid recipient, a rejected destination) are dead-lettered on the first attempt instead of burning all three.
Recipients
The recipient list is resolved in this order, first match wins:
recipientsin the/sendbody- The account's email defaults, if configured
- The invoice's own
email_config - The customer's billing emails
If nothing resolves, the call fails with 400. A draft invoice cannot be sent — issue it
first.
Reserved domains never send
Addresses on the reserved documentation domains and non-routable TLDs of RFC 2606 and RFC 6761 are dropped before the provider is called, because it would reject them permanently anyway:
| Rejected | Values |
|---|---|
| Domains | example.com, example.net, example.org (and any subdomain) |
| TLDs | .test, .example, .invalid, .localhost, .local |
A fixture customer with cliente@example.com produces a FAILED record and no email.
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. It is not one email per customer, and at
least one explicit recipient is required — nothing is inferred from the invoices.
| Limit | Value |
|---|---|
| Max invoices per request | 200 |
| PDFs attached individually | Up to 5 |
| PDFs bundled as a single ZIP | 6 or more |
| ZIP download link lifetime | 24 hours |
Invoices must be ISSUED, PAID, SENT or OVERDUE. Partial success is normal: if
some PDFs cannot be attached the email still goes out with the rest and the response
carries a failures array, so a 2xx does not mean all of them made it in.
Because it is one email, a 200-invoice bulk send costs one email against your quota, not 200 — see Sending a whole month at once for when that actually helps.
Checklist
- In sandbox, send only to your own address — use
+tagto separate test runs - Treat
403 ENVIO_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 - Never put
example.comor a.testaddress in fixtures - Pace month-end runs at ~1 email/minute, 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