NewProvince is only required for addresses in Spain
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog

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

FieldMeaning
frequencyMONTHLY, QUARTERLY or YEARLY — every 1, 3 or 12 months. Omitted, MONTHLY applies.
day_of_month1–31. The day each invoice is generated.
start_dateWhen 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_date in 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

ValueWhat happens
false (default)The invoice is generated and issued on the scheduled day.
trueA 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:

ModeHow
Open-endedNeither end_date nor max_invoices.
On a dateend_date.
After N invoicesmax_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

statusMeaning
ACTIVEGenerating on schedule.
PAUSEDNot generating. pause.reason says why: USER, DOWNGRADE (the account lost the recurring-invoices feature) or GENERATION_FAILURE.
COMPLETEDTerminal. 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" }'
  • PAUSED stops generation and keeps the schedule.
  • ACTIVE resumes. 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 becomes COMPLETED.
  • COMPLETED ends the template for good. From there, ACTIVE and PAUSED are rejected with 400 RECURRING_STATE_TRANSITION_INVALID, and every write — including a PATCH that only touches notes — answers 409 RECURRING_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.

FailureWhat 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

ActionEndpointWhat it does
SkipPOST …/skipSkips the next generation without issuing anything. The next date always lands after today. Only on ACTIVE templates.
Generate nowPOST …/generateGenerates 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 occurrenceGET …/next-occurrenceThe invoice the next generation would produce. Nothing is saved and no number is used, so invoice_number is null.
StatsGET …/statsCompany-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.

FieldMeaning
typeGENERATED, FAILED, SKIPPED or PAUSED (an automatic pause). Only GENERATED produces an invoice.
invoice_idThe 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).
originWho 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.
reasonHuman-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_nameWho skipped the slot. Only on SKIPPED entries.
scheduled_dateThe calendar slot the entry refers to.
generated_atWhen 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 / statusThe 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_amount adds up the next invoice of each template as is, so a quarterly template contributes its full quarterly amount. The two blocks (active and stopped) are never meant to be added together.
  • Generate-now before start_date is rejected with 422 RECURRING_NOT_STARTED: there is no occurrence to bring forward yet. Create a normal invoice instead.
  • A cadence and an end_date that leave no occurrence — a yearly template covering a calendar year already under way — are rejected with RECURRING_NO_OCCURRENCE_IN_WINDOW.
  • payment_iban, payment_swift and payment_term_days are only accepted together with a payment_method that is not null or NONE (422 PAYMENT_DETAILS_REQUIRE_METHOD).
  • PATCH replaces lines as a whole: send lines and the template keeps only the lines you sent. A PATCH without lines keeps them. The payment fields travel together: payment_iban, payment_swift or payment_term_days without payment_method in the same call answers 422 PAYMENT_DETAILS_REQUIRE_METHOD, even if the template already has a method. A PATCH that touches none of them keeps the payment data.
  • Changing invoice_type on a PATCH is judged on the resulting template, with the rules of the new type: the series has to accept it (422 SERIES_INCOMPATIBLE_DOC_TYPE), and a SIMPLIFIED template cannot keep an identified recipient (422 SIMPLIFIED_INVOICE_FORBIDS_IDENTIFIED_RECIPIENT) nor lines a simplified invoice does not admit. Send the new series_id, and customer_id: null when moving to SIMPLIFIED, in the same call. Without invoice_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.