Recurring invoices
Schedule invoices that generate themselves — cadence, first occurrence, review drafts, automatic sending, how a series ends, and what happens when a generation fails.
A recurring invoice is a template plus a schedule. The template holds what every invoice repeats (lines, recipient, series, payment); the schedule decides when the next one is generated. Every generated invoice is a normal invoice: it takes the next number of the template's series and goes through the same issuing rules as one you create by hand.
All operations live under /v1/companies/{company_id}/recurring-invoices.
How the schedule works
Cadence and day of the month
| Field | Meaning |
|---|---|
frequency | MONTHLY, QUARTERLY or YEARLY — every 1, 3 or 12 months. Omitted, MONTHLY applies. |
day_of_month | 1–31. The day each invoice is generated. |
start_date | When the subscription starts. |
A month that does not have day_of_month falls back to its last day: a template on 31
generates on 28 February (29 in a leap year) and on 30 April. So 31 is how you ask for "the
last day of the month". The adjustment never sticks — each date is recalculated from the value
you sent, so the schedule does not drift (31 Jan → 28 Feb → 31 Mar).
The first occurrence
The cadence governs the step, not where the first invoice lands. The first occurrence is
the first day_of_month on or after start_date, looked for one month at a time whatever the
cadence; from there the cadence takes over.
A YEARLY template starting 15 February with day_of_month: 10 first invoices on 10 March,
then every 10 March after that. It does not wait a year.
Generation runs once a day, and never looks back
Due templates are generated in a daily run early in the morning, Madrid time. An invoice is always issued with that day's date, so the system never back-fills:
- A
start_datein the past is accepted and stored as sent (useful when migrating subscriptions), but the first generation is the next date of the template's calendar that is still ahead — today included. On a quarterly or yearly template that can be months away: a quarterly template on day 10 that started on 10 January 2025, created on 24 September 2026, first generates on 10 October 2026. The periods already missed are not generated. - Resuming a paused template whose next date has passed reschedules it to the first occurrence strictly after today. The periods missed while paused are not generated either.
next_generation on the template is the date the system will actually honour, or null when
there is none (the template is COMPLETED, or PAUSED with its date already behind it). An
ACTIVE template can show a date in the past: that is the occurrence the next run is about to
generate.
Create a template
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/recurring-invoices" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2f1c7a52-8f0e-4c1b-9a4e-1d2b3c4d5e6f" \
-d '{
"name": "Monthly maintenance — Acme",
"frequency": "MONTHLY",
"day_of_month": 1,
"start_date": "2026-10-01",
"series_id": "{series_id}",
"invoice_type": "STANDARD",
"customer_id": "{customer_id}",
"lines": [
{
"description": "Website maintenance",
"quantity": 1,
"unit_price": 150.00,
"vat_rate": 21
}
],
"max_invoices": 12,
"draft_in_advance": true,
"send_automatically": true
}'Required: name, day_of_month, start_date, series_id, invoice_type (STANDARD or
SIMPLIFIED) and at least one line. Recurring lines use flat tax fields (vat_rate,
irpf_rate, equivalence_surcharge_rate…), not the main_tax object of an invoice line, and
each line carries exactly one of unit_price, total_excluding_tax or total_including_tax.
A template line does not inherit the company's default IRPF. Unlike an invoice line, a
template line sent without irpf_rate generates invoices with no withholding. If your
invoices carry IRPF, send irpf_rate on every template line.
The response carries the template with its next_generation, status: "ACTIVE" and amount
— what the customer will be asked to pay on the next invoice (base + VAT + surcharge −
withholding + disbursements).
Already have the invoice? Derive the template from it
If you have an invoice that already says what you want to repeat, derive the template from it and describe only the recurrence. Lines, recipient, series and payment data are taken from the source invoice, which is not modified.
curl -X POST "https://app.beel.es/api/v1/companies/{company_id}/recurring-invoices/derivations" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from_invoice_id": "{invoice_id}",
"name": "Quarterly support — Acme",
"frequency": "QUARTERLY",
"day_of_month": 31,
"start_date": "2026-10-01"
}'from_invoice_id, name, day_of_month and start_date are required. The cadence is never
copied from the source invoice — a one-off invoice has none — so frequency defaults to
MONTHLY here too.
Review drafts and automatic sending
draft_in_advance
| Value | What happens |
|---|---|
false (default) | The invoice is generated and issued on the scheduled day. |
true | A draft is created 5 days before the scheduled day so you can review it, and it is issued on the scheduled day. |
Either way the invoice is issued on the scheduled day; the only difference is whether there is a draft to look at first. The window is fixed at 5 days.
preview_days is deprecated and still accepted: any value above 0 means
draft_in_advance: true, 0 means false. If you send both, draft_in_advance wins.
send_automatically
With send_automatically: true, each generated invoice is emailed once issued, to the
recipients resolved as described in Sending email — the
template's email_configuration.recipients first, then the customer's email.
A template with send_automatically: true and no recipient that can be resolved from either
source is rejected on write with 422 SIN_DESTINATARIO_RESOLUBLE, instead of failing silently
every month. If the recipient loses its email address later, the invoice is still issued on
schedule — only the email is not sent.
Emails sent this way count against your send quotas.
How a series ends
A recurrence ends in exactly one way:
| Mode | How |
|---|---|
| Open-ended | Neither end_date nor max_invoices. |
| On a date | end_date. |
| After N invoices | max_invoices, between 2 and 600 (422 RECURRING_MAX_INVOICES_BELOW_MINIMUM / 422 RECURRING_MAX_INVOICES_ABOVE_MAXIMUM outside). |
Sending both end_date and max_invoices with a value is rejected with
RECURRING_END_MODE_CONFLICT. The conflict is judged on the resulting template, so a
PATCH that adds max_invoices to a template that already has an end_date is rejected too.
To switch modes, say both things in the same call:
curl -X PATCH "https://app.beel.es/api/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "max_invoices": 6, "end_date": null }'max_invoices is the total agreed, not what is left: remaining is
max_invoices − generated_invoices. It counts invoices generated: a skip does not spend it,
a generate-now does. Below 2 you do not want a recurrence but a
scheduled invoice.
When the last invoice is generated, or the next date falls past end_date, the template
becomes COMPLETED on its own.
States
status | Meaning |
|---|---|
ACTIVE | Generating on schedule. |
PAUSED | Not generating. pause.reason says why: USER, DOWNGRADE (the account lost the recurring-invoices feature) or GENERATION_FAILURE. |
COMPLETED | Terminal. completion.reason says why: USER, END_DATE_REACHED or MAX_INVOICES_REACHED. Treat completion.reason as an open set. |
You move a template between states with
PUT …/status:
curl -X PUT "https://app.beel.es/api/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/status" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "PAUSED" }'PAUSEDstops generation and keeps the schedule.ACTIVEresumes. It keeps the next date if it has not fallen due yet (today included), and reschedules it only if it is already in the past. If nothing is left to generate, the template becomesCOMPLETED.COMPLETEDends the template for good. From there,ACTIVEandPAUSEDare rejected with400RECURRING_STATE_TRANSITION_INVALID, and every write — including aPATCHthat only touches notes — answers409RECURRING_ALREADY_ENDED. A completed template can still be deleted.
When a generation fails
The daily run separates failures that waiting can fix from failures it cannot.
| Failure | What happens to the template |
|---|---|
| Transient (a timeout, the tax authority being down…) | Stays ACTIVE. is_failing becomes true from the first failure, a FAILED entry is added to the history, and the next day's run tries the same period again. is_failing clears after a successful generation. |
| Permanent (the company can no longer issue, a rejection that retrying will not change) | Moves to PAUSED with pause.reason: "GENERATION_FAILURE". pause.blocker names what blocked it, when the cause is a readiness one. A PAUSED entry is added to the history, and the recurring_invoice.paused webhook is delivered. |
Resuming is rejected while the blocker is still in effect. A template paused with a
pause.blocker answers 422 EMISSION_NOT_READY to {"status": "ACTIVE"} until that blocker
is resolved. Fix the cause first — pause.blocker uses the same codes as the blockers[] of
an EMISSION_NOT_READY — then resume.
Subscribe to recurring_invoice.paused if your
billing depends on these invoices. It is delivered when a template stops without anyone
asking — a permanent failure or a plan downgrade — and not when a person pauses it. It is
also delivered in Test.
To find templates that need attention, filter the list by pause_reason:
curl "https://app.beel.es/api/v1/companies/{company_id}/recurring-invoices?pause_reason=GENERATION_FAILURE" \
-H "Authorization: Bearer $BEEL_API_KEY"A transient failure does not pause anything, so it does not show up in that filter: look for
is_failing: true on ACTIVE templates.
Actions on a template
| Action | Endpoint | What it does |
|---|---|---|
| Skip | POST …/skip | Skips the next generation without issuing anything. The next date always lands after today. Only on ACTIVE templates. |
| Generate now | POST …/generate | Generates the pending occurrence now. It brings the period forward, it does not add one: next_generation advances one step. Answers 201 with invoice_id and the new next_generation (null when that was the last invoice). Only on ACTIVE templates. |
| Next occurrence | GET …/next-occurrence | The invoice the next generation would produce. Nothing is saved and no number is used, so invoice_number is null. |
| Stats | GET …/stats | Company-wide active and stopped blocks, each with count, monthly_amount and uncounted_count. |
Skip and generate-now on a template that is not ACTIVE answer 400 RECURRING_NOT_ACTIVE.
Generating manually, skipping and letting the schedule run each consume exactly one occurrence, however you mix them. Generate-now is a fiscal act: the invoice takes a number and, if the template says so, is issued and sent.
Need an extra invoice outside the calendar? Don't use generate-now — it consumes the
next period. Create a normal invoice, or derive one from an invoice the template already
generated with POST …/invoices/derivations.
History
GET …/history returns every entry
of the schedule, newest first and paginated (20 per page by default). It tells you not only
which invoices were generated but also why a period has no invoice.
| Field | Meaning |
|---|---|
type | GENERATED, FAILED, SKIPPED or PAUSED (an automatic pause). Only GENERATED produces an invoice. |
invoice_id | The generated invoice, or null — always null for FAILED, SKIPPED and PAUSED, and also when the invoice is no longer reachable (for example a draft that was deleted). |
origin | Who produced the entry: MANUAL (someone asked) or UNATTENDED (the daily run). Generations carry it, and so does a FAILED entry, always UNATTENDED: only the daily run records failures. |
reason | Human-readable explanation of a FAILED or PAUSED entry, in the caller's language. Presentation text: branch on type, not on this. |
requested_by / requested_by_name | Who skipped the slot. Only on SKIPPED entries. |
scheduled_date | The calendar slot the entry refers to. |
generated_at | When the entry was written: the moment of the generation, the skip, the failure or the pause. On a FAILED entry it is the time of the failed attempt, not of an invoice. |
invoice_number / total / status | The generated invoice's current number, total and status. Only on GENERATED entries whose invoice is still reachable. |
Fields that do not apply to an entry may be missing instead of null: read them as optional.
A generate-now and a skip, as the history returns them:
[
{
"id": "d9f2661c-3028-4a06-b948-45598da919bc",
"type": "GENERATED",
"invoice_id": "4dfe3e8e-7977-4b39-90d4-6eac1efdcf8d",
"origin": "MANUAL",
"reason": null,
"requested_by_name": null,
"generated_at": "2026-09-24T07:38:28.488041Z",
"scheduled_date": "2026-09-24",
"invoice_number": "VN/000153",
"total": 121,
"status": "ISSUED"
},
{
"id": "b05246e2-b215-4f0e-940b-1cfe5dedc1e2",
"type": "SKIPPED",
"invoice_id": null,
"reason": null,
"requested_by": "395176a9-438f-4077-96aa-4ebee6185f0e",
"requested_by_name": "…",
"generated_at": "2026-09-24T07:37:44.464148Z",
"scheduled_date": "2026-10-01"
}
]A FAILED entry has invoice_id: null, origin: "UNATTENDED" and a reason.
Handle invoice_id: null. Code that assumed every history entry points to an invoice
breaks on FAILED, SKIPPED and PAUSED entries.
Gotchas
- Quarterly and yearly amounts are not normalised in the stats.
monthly_amountadds up the next invoice of each template as is, so a quarterly template contributes its full quarterly amount. The two blocks (activeandstopped) are never meant to be added together. - Generate-now before
start_dateis rejected with422RECURRING_NOT_STARTED: there is no occurrence to bring forward yet. Create a normal invoice instead. - A cadence and an
end_datethat leave no occurrence — a yearly template covering a calendar year already under way — are rejected withRECURRING_NO_OCCURRENCE_IN_WINDOW. payment_iban,payment_swiftandpayment_term_daysare only accepted together with apayment_methodthat is notnullorNONE(422PAYMENT_DETAILS_REQUIRE_METHOD).PATCHreplaceslinesas a whole: sendlinesand the template keeps only the lines you sent. APATCHwithoutlineskeeps them. The payment fields travel together:payment_iban,payment_swiftorpayment_term_dayswithoutpayment_methodin the same call answers422PAYMENT_DETAILS_REQUIRE_METHOD, even if the template already has a method. APATCHthat touches none of them keeps the payment data.- Changing
invoice_typeon aPATCHis judged on the resulting template, with the rules of the new type: the series has to accept it (422SERIES_INCOMPATIBLE_DOC_TYPE), and aSIMPLIFIEDtemplate cannot keep an identified recipient (422SIMPLIFIED_INVOICE_FORBIDS_IDENTIFIED_RECIPIENT) nor lines a simplified invoice does not admit. Send the newseries_id, andcustomer_id: nullwhen moving toSIMPLIFIED, in the same call. Withoutinvoice_type, the type is kept. - VeriFactu is not a template setting. Whether each generated invoice is registered with the AEAT is decided when that invoice is issued, from the company's regime at that moment.
Related
Series and numbering
When an invoice gets its number, how the format and counter resets work, default series per document type, and why a series locks once it has issued.
Proformas
Send a customer a formal quote with no fiscal validity, then turn it into a real invoice once they accept — as a draft or issued in the same call.