Perfex CRM Recurring Invoices: Setup, Cron and Failures
Perfex CRM recurring invoices fail quietly. The auto-send, payment mode and cron settings that decide renewal day - and how to make invoices collect themselves.
It is the 6th of the month. A client emails asking where their invoice is, and you open the CRM to find the parent sitting exactly where you left it: cycle counter unmoved, no red banner, nothing on screen suggesting anything went wrong. Perfex CRM recurring invoices do not fail loudly. They simply stop happening, and the first person to notice is usually the customer who was trying to pay you.
Most write-ups stop at the Repeat every dropdown, as if picking an interval were the whole job. It is the least interesting part. Three other things decide what happens on renewal day, and one of them is not on the invoice screen at all.
We build and sell Perfex modules, including the Stripe SEPA gateway named later on, so weigh the recommendations with that in mind. The diagnostic order below holds regardless of what you have installed.
What “Repeat every” actually schedules
Recurring is not a separate object in Perfex. It is a set of columns on an ordinary invoice row: recurring, recurring_type for custom intervals, cycles and total_cycles, and last_recurring_date. The invoice you flagged is the parent, and it stays the parent forever. Each renewal produces a new invoice with its own number, copies the line items across, and stamps the parent with the date it last fired.
Three consequences follow, and they surprise people:
- The next date is computed, not stored. There is no queue of pending future invoices you can open and inspect. The next renewal is last fire plus interval, evaluated at the moment a background job asks.
- Cycles is a stop, not a counter of successes. Set it to 12 and Perfex creates twelve invoices and stops. Leave it at zero and it bills forever. A twelve-month retainer set up in a hurry with cycles left at zero will still be billing in month thirteen, to a client who left in month eleven.
- The child’s due date is recalculated, not inherited. It comes from your default invoice due-after setting, so a parent you hand-edited to net-30 can quite happily produce net-7 children. Check one real renewal before trusting it.
There is a useful query for this. Run it against your database and you have the entire recurring book on one screen:
SELECT id, number, clientid, recurring, recurring_type,
cycles, total_cycles, last_recurring_date
FROM tblinvoices
WHERE recurring > 0;
Anything whose last_recurring_date is older than its own interval is already broken, whether or not anyone has complained yet.
The three settings that decide renewal day
Auto-send: creating and sending are different jobs
Creating the invoice and putting it in front of the customer are two separate decisions in any Perfex CRM recurring billing setup, and the second one has an off switch. Depending on your version it lives on the invoice’s recurring options or as a global toggle under Setup > Settings > Cron Job. Find yours before you assume anything.
With it off, invoices pile up in Unpaid and nobody outside the building knows they exist. With it on, the email leaves at whatever hour your background job happens to run, which is worth thinking about if that is 03:00 server time and half your clients are five hours away.
The decision rule is simple: auto-send on for anything with fixed line items, off for anything where a human adds usage, expenses or an extra day before it goes out. Do not compromise by turning it on and promising yourself you will check.
Payment mode carry-over: the parent is the template
The allowed payment modes on the child are copied from the parent. This is quietly responsible for a lot of “why can this client only pay by bank transfer” tickets: the parent invoice was created in March, you enabled a new gateway in June, and every renewal since has been produced from a template that predates it.
Fixing the child that just went out does nothing for next month. Edit the parent, tick the modes you want there, and only then reissue. The same logic applies to tax rates and the assigned currency - the parent is the mould, and reshaping one casting does not reshape the mould.
The cron: the part that is not in the CRM at all
Nothing above happens without a background job. Perfex has one entry point, a PHP file under the install root (crons/cron.php on a standard layout), and it does not only handle invoices. The same run fires overdue reminders, recurring expenses, task reminders, contract expiry notices, ticket email import and ticket auto-close.
That single fact explains a lot of confusing tickets: when the cron dies, five unrelated features break at once and none of them says why. A “perfex recurring invoice not creating” report and a “reminders stopped” report are usually one incident.
A sane crontab line runs every five minutes with an absolute binary path:
*/5 * * * * /usr/local/bin/php -q /home/user/public_html/crons/cron.php >/dev/null 2>&1
Use the full path. A bare php in a crontab resolves against a minimal PATH and often lands on a different, older binary than the one serving the site, possibly missing extensions or reading a different php.ini.
When Perfex CRM recurring invoices stop appearing, work in this order
Cheapest checks first. Do not open the invoice until step six.
- Read the last cron run timestamp in
Setup > Settings > Cron Job. If it is stale, stop. This is not an invoicing problem and no amount of editing the invoice will help. - Run the cron by hand from the shell and read what it prints:
php -q /home/user/public_html/crons/cron.php. A fatal error, an exhausted memory limit or a database connection failure announces itself here in a way it never does running silently under cron. - Check
max_execution_timeandmemory_limitfor the CLI binary specifically. php-cli usually loads a different ini from php-fpm. A run that dies at 30 seconds completes the jobs earlier in the sequence and never reaches recurring invoices, which produces the maddening symptom of some automation working and some not. - Check whether cycles ran out.
total_cyclesreachingcyclesis a silent, correct, by-design stop. It looks identical to a failure from the outside. - Check the parent survived editing. Opening a recurring invoice, changing something and saving without re-selecting the interval clears the schedule. So does cancelling or converting the parent.
- Now look at the invoice itself. In our experience this is the answer perhaps one time in twenty.
Calendar day or anniversary: the decision nobody makes deliberately
Almost every install ends up on anniversary billing by accident, because “repeat every 1 month” from the date you happened to create the invoice is what the form gives you when you are not thinking about it. The alternative is billing every client on the 1st. Both are defensible. Drifting into one is not.
| Everyone on the 1st | Each client’s anniversary | |
|---|---|---|
| Cash arrival | One lump, easy to forecast | Smooth, roughly 1/30th a day |
| Part-period at signup | Needs a manual pro-rata invoice | None, the period starts when they do |
| Chasing late payers | One concentrated window you can staff | Continuous background task |
| A day when the cron is dead | 100% of the month’s billing | Around 3% of the book |
| Support load | Spike on the 1st to 3rd | Flat |
The dunning line is the one that matters, and it is the one people get backwards. Concentrated billing looks worse because the chasing is visible, but visible work gets done: one person, three days, a list that ends. Anniversary billing spreads the same work so thinly that it never becomes anyone’s job, and unpaid invoices age quietly because no particular day is ever the day you deal with them.
A workable rule: under about 40 recurring clients, anniversary billing is fine and saves you the pro-rata arithmetic. Above that, or any time one person owns collections, move to calendar billing. Migrating is dull rather than hard: let the current period finish, stop the old parent, issue one short pro-rata invoice bridging to the 1st, then create a new parent dated the 1st. Note the trade you are accepting - calendar billing is the model that makes monitoring the cron non-optional.
Removing the ask entirely
Every setting above optimises the same fundamentally weak flow: produce a document, email it, wait for a human to act. Auto-send makes the email prompt. It does not make anyone pay.
Mandate-based collection inverts that. The customer authorises once, and after that the money moves without a decision from them. Our Stripe SEPA Direct Debit gateway for Perfex does this against SEPA-enabled bank accounts: once a payment is authorised it is captured automatically, built on Stripe’s Payment Intents API and kept current as that API evolves. Setup is entering your Stripe API settings inside Perfex - genuinely about a minute of work - and bundled documentation covers uploading and activating the module, with a French translation included.
Because payment modes carry over from parent to child, adding it to the parent invoice means every subsequent renewal offers it without further intervention.
Two honest limits. It only covers SEPA-enabled bank accounts, and bank debits settle more slowly than a card, which suits year-long relationships rather than one-off projects. Watch one live renewal cycle before assuming the whole book is on autopilot.
Do these six things this week
- Run
php -q /path/to/crons/cron.phpfrom the shell right now and read every line of output. - Fix the crontab to use the absolute path to the correct PHP binary, every five minutes.
- Run the
tblinvoicesquery above and flag every row whoselast_recurring_dateis older than its interval. - Open one parent invoice per client type and confirm four things: interval, cycles, allowed payment modes, auto-send.
- Write down whether you bill on the 1st or on anniversaries, and why. One sentence in your operations notes is enough to stop the drift.
- Set a calendar reminder for the day after your next renewal date to confirm the invoice exists and was sent.
A copy of this article is also published on dev.to, where the comments are open.