Gateway setup

Recurring Payments collects money on three rails: Stripe (cards worldwide), GoCardless (bank debit in the UK and Europe) and Paystack (cards in Africa). You do not have to enable all three. Set up the one your customers actually pay with; the others can stay switched off forever.

Every gateway needs exactly two things from you, and both must be done or nothing will charge automatically:

  1. Credentials, entered once on Setup → Settings → Payment Gateways.
  2. A webhook, registered in the gateway's own dashboard, pointing back at your Perfex site. This is how the gateway tells the module that money arrived, a card failed, or a mandate was cancelled. Without it the module can start a payment but will never learn what happened to it.

How to read this page. Each gateway section is self-contained and always follows the same order: the account you need, the settings fields, the webhook, how a customer saves a payment method, a test run, and the behaviour you must understand before you go live. Read the section for your gateway top to bottom once. Sections marked gateway's own product describe how Stripe, GoCardless or Paystack behave, not what this module does — always check their current documentation for anything commercial or regulatory.

Which gateway do I need?

  Stripe GoCardless Paystack
Instrument Card (and wallets such as Apple Pay / Google Pay, which arrive as cards) Bank debit mandate (Bacs, SEPA and the other schemes GoCardless supports) Card
Typical region Worldwide, wherever Stripe accepts your business UK and Europe primarily; scheme depends on the payer's country Africa — Nigeria, Ghana, South Africa, Kenya
Default currency list in this module USD,EUR,GBP GBP,EUR NGN,GHS,ZAR,KES
How fast you know Seconds. The charge succeeds or declines during the API call. Days. The payment sits in transit until the bank confirms it. Seconds, unless the bank asks the cardholder for a PIN or OTP.
Who retries a failure This module's dunning engine. GoCardless. This module never retries a bank debit. This module's dunning engine.
Cost of a failure A decline code, immediately actionable. A failure days later, often with a bank charge attached. A decline code, immediately actionable.
Best for Anyone billing internationally, and anyone who wants an answer today. Predictable monthly retainers in the UK/EU where card churn hurts. African subscription businesses collecting in local currency.

Running two rails side by side is normal and fully supported: for example Stripe for international customers and GoCardless for domestic ones. Each customer's saved payment method carries its own gateway, and the module always charges the method that customer actually authorised.

Before you start

The settings screen

All three gateways are configured in one place: Setup → Settings → Payment Gateways. Once the module is active you get one extra tab per rail, alongside whatever gateways Perfex or other modules already provide:

These are separate from any Stripe or other gateway Perfex already ships. They are named this way so you can tell them apart in the admin area; what your customers see is the Label field inside each tab, which you should shorten.

Setup, Settings, Payment Gateways with the three additional gateway tabs

Every tab starts and ends with the same two settings, which come from Perfex itself:

FieldWhat to put in it
Active Set to Yes. While this is No the gateway does not appear on any invoice and customers cannot save a payment method with it.
Label The name your customers see on the invoice payment screen. It is pre-filled with the long tab name above, which is fine for you and wrong for them — shorten it to something like Credit / debit card or Direct Debit. Renaming it here changes nothing technical.
Selected by default on invoice Whether this is the pre-selected payment option when a customer opens an invoice. If you enable two gateways, only one should be default.

Encrypted fields. Fields marked encrypted in the tables below are stored encrypted in the database using your Perfex encryption key and are decrypted only at the moment an API call is made. They are never written to the module log, never sent in an e-mail, and never shown on the Diagnostics screen — Diagnostics reports only whether a secret is present. If you rotate a key in the gateway dashboard, paste the new value here; there is nothing else to change.

Where to find your webhook URL

Do not type the webhook URL by hand. Open Recurring Payments → Settings → Diagnostics: each gateway block shows a read-only Webhook URL box containing the exact URL your installation generates, including any subfolder your CRM lives in. Copy it from there and paste it into the gateway dashboard.

The three endpoints follow this pattern:

https://your-crm.example.com/autobill/autobill_webhook/stripe
https://your-crm.example.com/autobill/autobill_webhook/gocardless
https://your-crm.example.com/autobill/autobill_webhook/paystack

autobill is the module's internal folder name. It appears in URLs, database table names and CSS class names, and it must not be renamed — renaming it breaks the installation. It has no bearing on what your customers see.

All three endpoints behave identically, and every gateway here redelivers on a non-2xx answer, so a temporary problem on your side is never a lost payment:

AnswerMeaning
200Handled, or deliberately ignored. Nothing left to do.
400The signature did not verify, or the body could not be read. Nothing was written. Usually a wrong or missing signing secret.
405The request was not a POST. Opening the URL in a browser always gives this — that is the correct answer, not a fault.
500A temporary failure on this side. The gateway should retry.
503The module is deactivated, or that gateway has no credentials yet.

Every accepted event is stored once, keyed on the gateway plus the gateway's own event id, so a redelivery of an event that was already processed is recognised and skipped. Redeliveries are normal traffic, not an incident.

The currency list, and the trap it causes

Every gateway tab has a field labelled Currencies (coma separated). It is the single most common cause of "the module is not charging anything", because of how Perfex treats it.

If an invoice's currency is not in a gateway's currency list, Perfex removes that gateway from the invoice silently. There is no warning, no error and no log entry on the invoice. The customer simply sees an invoice with no way to pay, and the module refuses to charge it off-session rather than sending a request the gateway would reject.

This bites in three ordinary situations:

How to diagnose it

The Diagnostics screen exists for exactly this. Open Recurring Payments → Settings → Diagnostics.

The Diagnostics tab showing per-gateway credentials, webhook URL and enabled currencies
  1. Look at the Base currency tile at the top. That is what invoices use unless a customer overrides it.
  2. Find your gateway's block. The Enabled currencies row lists exactly what that gateway's currency field contains.
  3. If a currency exists in Perfex but is missing from the gateway, the row shows a red line reading Not enabled for: XXX — invoices in these currencies cannot be charged with this gateway. That is your answer.
  4. If the field is empty you get No currencies enabled — this gateway will not appear on any invoice. An empty list is not "allow everything"; it is a dead gateway.
  5. Fix it with the Open gateway settings button, add the missing code to the comma-separated list, save, and reload Diagnostics. The red line disappears.

Two rules keep you out of trouble: only list currencies your gateway account can actually settle, and add a currency to your gateways at the same moment you add it to Perfex.

The same screen answers the other silent stoppers:

RowWhat it means when it is wrong
API credentials — MissingThe gateway has no secret key (or, for GoCardless, no access token). Nothing can be charged and the webhook endpoint answers 503.
Webhook signing secret — MissingShown for Stripe and GoCardless only. Deliveries will be rejected with 400, so payments are started but never confirmed. Paystack has no such field by design; see its section.
Unprocessed webhooks above zeroEvents arrived but could not be processed. Cron retries them on its next run; a number that stays high needs the module log.
Stuck attempts above zeroCharges opened more than an hour ago that are still pending or processing and were never resolved by a result or a webhook. Bank debits in transit are not counted here.
Engine self-checkCron age, per-job state, how many subscriptions are waiting for a payment method, and the payment methods grouped by status. Its own attempts_stuck figure is the wider one: it also counts GoCardless payments still in transit, so a non-zero value there is normal while a collection is in flight.

Stripe

The account you need

A standard Stripe account with card payments enabled. There is no special plan, product or add-on to buy, and you do not need Stripe Billing or Stripe Subscriptions — this module does the recurring logic itself and uses Stripe purely to store a card and charge it.

Gateway's own product: Stripe must support your country for payouts, and your account must be allowed to save cards for later use. Brand-new accounts are sometimes restricted until Stripe finishes verifying the business; if saving a card works but charging it later fails with an account-level error, that is a Stripe onboarding question, not a module problem.

The module talks to Stripe using the Stripe PHP library that ships inside Perfex, so there is nothing to install. It pins the Stripe API version it was written against, and it keeps its own API key strictly separate from any key you may have entered in Perfex's built-in Stripe gateway — the two never overwrite each other.

Settings fields

The Stripe gateway tab with its settings fields
FieldValueEncrypted
Stripe publishable key Starts pk_live_ or pk_test_. Stripe Dashboard → Developers → API keys. Used by one screen only: the page a customer lands on to complete a 3-D Secure challenge. Leave it empty and every 3-D Secure recovery link you send will dead-end, so fill it in even though most payments never touch it. No
Stripe secret key Starts sk_live_ or sk_test_, same Dashboard page. Every API call the module makes uses this key, and its prefix is what tells the module whether it is in test or live mode — there is no separate switch. A restricted key must be able to read and write Customers, PaymentMethods, SetupIntents, PaymentIntents, Checkout Sessions, Charges and Refunds. Yes
Stripe webhook signing secret Starts whsec_. It is created together with the endpoint in the next step, not on the API keys page. Every delivery is verified against it; without it nothing is processed. Yes
Save the payment method for automatic charging by default Yes (the default) means that when a customer pays an invoice interactively through this gateway, the card is also stored for future automatic charging. No means each payment is a one-off and nothing is kept. This only affects the interactive "Pay now" flow; a customer who deliberately adds a payment method in the portal always stores it. No
Currencies (coma separated) Defaults to USD,EUR,GBP. List every currency you invoice in. See the currency trap. No

The webhook

In the Stripe Dashboard go to Developers → Webhooks → Add endpoint, paste the URL from Diagnostics:

https://your-crm.example.com/autobill/autobill_webhook/stripe

We deliberately do not reproduce the Stripe dashboard here. Stripe redesigns it periodically, and a screenshot that no longer matches what you are looking at is worse than none — the URL above and the settings below are what actually matter, and they do not change.

Then subscribe to exactly these six events. These are the only Stripe events the module acts on; anything else you select is accepted, stored and ignored, which just fills the events table:

EventWhat the module does with it
payment_intent.succeededWrites the payment into Perfex against the invoice. This is the only place money is recorded — not the browser redirect. If a customer pays and the invoice still says unpaid a minute later, this event is not arriving.
payment_intent.payment_failedMarks the charge attempt failed, stores the decline and advice codes, and hands the invoice to the dunning engine.
setup_intent.succeededFallback for saving a card when the customer closed the browser tab before being redirected back.
charge.dispute.createdMarks the attempt as charged back and notifies your staff. The Perfex payment record is deliberately left alone — reversing money is a human decision.
payment_method.automatically_updated
payment_method.card_automatically_updated
Stripe's card updater refreshed the stored card. Brand, last four digits and expiry are updated in place. Stripe renamed this event, and the dashboard subscribes an endpoint to both spellings — the module handles both, so tick whichever your account offers. See the re-consent note below.
payment_method.detachedThe card was removed at Stripe. The stored method is marked revoked so it is never charged again.

Stripe shows the signing secret once the endpoint exists (Reveal under Signing secret). Copy it into Stripe webhook signing secret and save.

Test mode and live mode have different signing secrets. Create the endpoint twice — once in test mode, once in live — and remember to swap both the secret key and the signing secret when you go live. A live secret key paired with a test signing secret produces the classic symptom: charges work, confirmations never arrive.

How a customer saves a card

From the client portal: Billing & Cards → Add payment method. They choose Credit / debit card, tick the authorisation statement, and are sent to a Stripe Checkout page in setup mode — Stripe's own hosted page, on Stripe's domain.

The client portal page for adding a payment method

Setup mode means no money is taken. Stripe validates the card, may run a 3-D Secure challenge, and hands back a token that is explicitly marked for off-session use. The module refuses to store a token that is not marked that way, because such a token would be rejected on the first automatic charge.

What is stored on your server: the token, the Stripe customer id, and the card's cosmetic details (brand, last four digits, expiry, funding type, country). Together with a consent record: the exact authorisation text the customer accepted, the timestamp, their IP address and their browser user agent. The card number and CVC never touch your server — they are typed on Stripe's page.

One end-to-end test

  1. Enter your sk_test_ and pk_test_ keys, set Active to Yes, and make sure the currency list contains the currency you will test with. Save.
  2. Create the test-mode webhook endpoint and paste its whsec_ secret. Save.
  3. Open Recurring Payments → Settings → Diagnostics. Stripe should show Enabled, credentials Set, webhook signing secret Set, and your currency in the list, with no red text.
  4. Log in to the client portal as a test customer and add a card with Stripe's published test card 4242 4242 4242 4242, any future expiry, any CVC. Check Stripe's testing documentation for the current list; these numbers are Stripe's, not ours.
  5. As staff, open Recurring Payments → Payment Methods. The card should be listed, active, and set as that customer's default.
  6. Give that customer a subscription with an open invoice, open it, and press Charge now. You should see Charge queued.
  7. Open Recurring Payments → Charge Attempts. Within seconds the row should read Succeeded with a gateway transaction id under the invoice number, and the invoice itself should now carry a payment.
The Charge Attempts screen after a successful test charge

Then test the unhappy path, which is what you actually bought this module for. Repeat with Stripe's decline test card (4000 0000 0000 9995 is Stripe's published insufficient-funds card): the attempt should read Failed with a decline code, a next-retry date should appear, and the first dunning e-mail should be queued. Then try Stripe's authentication-required card (4000 0025 0000 3155): the attempt should read Requires Authentication and your customer should receive a confirmation link.

If Charge now says No charge was made, the engine refused before touching Stripe. The three reasons are: the invoice currency is not in the gateway's list, the gateway has no credentials, or that payment method has hit the safety cap of 8 attempts in 14 days. Diagnostics tells you which.

Stripe behaviour you must understand

Off-session charging. Automatic charges are merchant-initiated: the customer is not present and cannot be asked anything. The module flags them to Stripe as off-session, which is what lets the issuer approve them under a stored mandate. This is also why the authorisation the customer accepts in the portal matters — it is the mandate, and it is recorded with a timestamp, IP and user agent for exactly this reason.

3-D Secure produces a recovery e-mail, not a retry. When an issuer demands authentication for an off-session charge, retrying it off-session will fail forever — only the cardholder can clear it. The module recognises this, stores the reference Stripe gives it, and e-mails the customer a single-use link to a page where they complete the challenge for that same payment. Two consequences: the publishable key must be filled in, and the link expires — 72 hours by default, set by Authentication link lifetime (hours) in the module settings. That page needs JavaScript, because the bank's security check does; it carries a fallback pointing at the invoice's own payment page for customers who have JavaScript disabled.

A card-brand change requires new consent. If Stripe's automatic card updater replaces a stored card with one on a different network — a Visa that becomes a Mastercard, for example — that is a different card, and the authorisation your customer gave no longer describes what you would charge. The module disables the stored method, marks it as needing re-consent, and asks the customer to authorise again. A same-network update (new expiry date or a re-issued number) is applied silently, as it should be.

Disputes. A dispute marks the attempt Charged Back and notifies staff. The Perfex payment record is not deleted and the invoice is not reopened automatically; you decide what to do, because the invoice may already carry credit notes or partial settlements.

Cards issued in India. Gateway's own product: the Reserve Bank of India requires recurring card payments on Indian-issued cards to be registered as an e-mandate with the issuer, with its own pre-debit notification and approval flow. This module does not register RBI e-mandates. It stores the issuing country on the payment method, and if Stripe reports an Indian recurring mandate as cancelled it stops immediately and asks the customer for a new payment method instead of retrying. If most of your customers hold Indian-issued cards, expect off-session charges to be declined and plan on invoice-by-invoice interactive payment until e-mandate support ships.

"Pay now" on an invoice. Customers can also pay a single invoice interactively through this gateway; they go to a Stripe Checkout page and come back to the invoice. The payment is recorded from the webhook, not from the browser redirect, so the invoice can read "unpaid" for a few seconds after they return. That is deliberate: a customer's browser is not a trustworthy source of truth about money.

GoCardless

The account you need

A GoCardless account for your business, plus a sandbox account for testing — GoCardless keeps them entirely separate, with different logins, different access tokens and different webhook secrets.

Gateway's own product: which debit schemes you can collect on depends on where your business is registered and where your payers bank; GoCardless selects the scheme from the payer's country during the authorisation flow. Your GoCardless account must be verified and activated before live collections work. Retries are handled by GoCardless's own retry product — see the retry note below, because it changes how this module behaves.

Eligibility caveat. Bank debit is a mandate scheme, not a card scheme. Your customer authorises you to pull money from their account, the scheme rules require them to receive advance notice before each collection, and they can claim the money back. Do not enable GoCardless without reading the compliance page.

Settings fields

The GoCardless gateway tab with its settings fields
FieldValueEncrypted
GoCardless access token Created in the GoCardless dashboard under Developers → Create access token, with read-write access. The sandbox dashboard issues its own token — a sandbox token will not work against live and vice versa. Yes
GoCardless webhook endpoint secret You choose or are shown this when you create the webhook endpoint in the next step. Every delivery is verified against it. Yes
Environment — type sandbox or live A free-text field. Type exactly live to talk to GoCardless's live API; anything else, including a typo, is treated as sandbox. That is deliberate — a misspelling can never accidentally collect real money. The current value is shown as a badge on the Diagnostics screen, so check it there after you go live. No
Currencies (coma separated) Defaults to GBP,EUR. Only list currencies your GoCardless account can actually collect. See the currency trap. No

Two further GoCardless settings live with the module rather than the gateway, at Recurring Payments → Settings → General:

SettingWhat it does
GoCardless advance notice Locked to GoCardless sends it. See the advance-notice note below. The alternative is visible but disabled on purpose, so that you can see who is responsible.
Record GoCardless payment in Perfex when Either Payment is confirmed (the default, recommended: the collection has succeeded at the bank) or Payment is paid out to your bank (conservative: the money has actually landed in your account, which is later). This decides when the invoice is marked paid.

The webhook

In the GoCardless dashboard, create a webhook endpoint under Developers → Webhook endpoints pointing at the URL from Diagnostics:

https://your-crm.example.com/autobill/autobill_webhook/gocardless

We deliberately do not reproduce the GoCardless dashboard here. GoCardless redesigns it periodically, and a screenshot that no longer matches what you are looking at is worse than none — the URL above and the settings below are what actually matter, and they do not change.

Copy the endpoint secret into GoCardless webhook endpoint secret and save. Deliveries are signed with a keyed hash of the raw request body, and a delivery that does not verify is refused with 400 before anything is read from it.

GoCardless batches several events into a single delivery. The module acts on the events below and answers 200 to everything else it is sent:

Resource and actionWhat the module does with it
paymentsconfirmedWrites the payment into Perfex, if Record payment when is set to confirmed. This is where a bank debit becomes a Perfex payment.
paymentspaid_outRecords the payment when you chose the conservative setting. It is also accepted as a stand-in for a confirmed event that never arrived, so a missed delivery cannot strand a real payment.
paymentsfailedMarks the attempt failed with the bank's cause and description, and starts dunning e-mails. It does not schedule a retry — see below.
paymentscharged_back, chargeback_settledMarks the attempt charged back and notifies staff. The Perfex payment is left in place for you to reverse deliberately.
mandatescancelled, expired, failedDisables the stored mandate so it is never charged again, and e-mails the customer to set up a new one.

How a customer sets up a mandate

From the client portal: Billing & Cards → Add payment method → Bank debit (Direct Debit). After ticking the authorisation statement they are sent to GoCardless's own hosted authorisation flow, where they enter their bank details and confirm the mandate. GoCardless picks the right scheme for their country.

When they return, the module reads the mandate back from GoCardless, stores the mandate reference, the bank name, the last digits of the account, the country and the scheme, plus the same consent record kept for cards (text, timestamp, IP, user agent). Bank account numbers never touch your server.

If the customer completed the bank authorisation but the flow was not finalised — a closed tab at the wrong moment — the module finalises it once on their return rather than making them start again.

One end-to-end test

  1. Enter your sandbox access token, set Environment to sandbox (or leave it), set Active to Yes, and check the currency list. Save.
  2. Create a webhook endpoint in the sandbox dashboard and paste its secret. Save.
  3. Diagnostics should show GoCardless Enabled with a sandbox badge, credentials Set and webhook signing secret Set.
  4. As a test customer, add a bank debit method and complete GoCardless's sandbox authorisation flow with the test bank details their sandbox documentation gives you.
  5. As staff, confirm the mandate appears under Payment Methods.
  6. Press Charge now on that customer's subscription.
  7. Open Charge Attempts. The row will read In Transit, with a GoCardless payment id. This is success, not a problem. It stays that way until GoCardless confirms the collection, at which point the same row becomes Succeeded and the invoice gets its payment. In sandbox you can move the payment along from the GoCardless dashboard instead of waiting.

GoCardless behaviour you must understand

Bank debit is not instant, and nothing about that is a fault. A card either works or does not within seconds. A bank debit is submitted, sits in transit while the scheme processes it, and only then confirms or fails — typically several working days, depending on the scheme. During that window the attempt reads In Transit and the invoice is still unpaid. The Stuck attempts tile on Diagnostics deliberately does not count these; the engine self-check's own attempts_stuck figure does, so expect a non-zero number there whenever a collection is in flight.

The Perfex payment is written on confirmation, not on submission. Which event counts as confirmation is your choice: confirmed (default) or the later paid_out. Choose paid_out if your accounting must never show money you have not received; choose confirmed if you want customers to see the invoice settled as soon as the collection succeeds.

GoCardless owns the retries; this module never retries a bank debit. Every payment the module creates is flagged as retryable at GoCardless, so their retry product decides when to try again for recoverable failures such as insufficient funds. If this module also scheduled a retry, you would collect twice from one customer for one invoice, and each failed attempt can carry a bank charge. So on a GoCardless failure the module sends the dunning e-mails and explicitly clears any next-retry date. The Next retry column staying empty for a failed GoCardless attempt is correct behaviour. The one thing the module will do is stop permanently and ask for a new mandate when the failure means the mandate itself is dead (cancelled, expired, failed, or a closed bank account).

Advance notice is GoCardless's job. Gateway's own product: debit schemes require the payer to be notified before each collection, with a scheme-defined minimum notice period. GoCardless sends these notifications for you by default. This module does not send pre-debit notices, in 1.1 any more than in 1.0, and the setting is therefore locked to GoCardless sends it. Leave GoCardless's own notifications switched on. If you turn them off in the GoCardless dashboard, nobody is notifying your payers and you are out of compliance with the scheme rules. Sending notices from the module is not part of this release; if a later one adds it, the setting unlocks then.

Chargebacks and indemnity claims. Gateway's own product: bank debit schemes give payers the right to claim a collection back, in some schemes long after the fact and without needing your agreement. When GoCardless reports a chargeback the module marks the attempt Charged Back and notifies your staff; it never silently deletes the Perfex payment, because reversing money is your decision and the invoice may already have credit notes against it. Keep the consent record the module stores for every mandate — it is your evidence that the customer authorised the collection.

Paystack

The account you need

A Paystack business account with card payments enabled. No special plan is required and you do not need Paystack's own subscription product — the module manages the billing cycle and uses Paystack only to store a reusable card authorisation and charge it.

Gateway's own product: Paystack supports a specific set of countries and currencies; your account can collect in the currencies your Paystack business is enabled for and no others. Card is the only channel that yields a reusable authorisation, which is why the module restricts the setup flow to cards.

Settings fields

The Paystack gateway tab with its settings fields
FieldValueEncrypted
Paystack public key (pk_...) From the Paystack dashboard, Settings → API Keys & Webhooks. The module never uses it — every customer-facing step happens on Paystack's own hosted page — but fill it in so the pair stays together and a future version has it. No
Paystack secret key Starts sk_test_ or sk_live_, from the same page. This one key does two jobs: it authenticates every API call, and it is the key Paystack uses to sign webhook deliveries. Rotate it in the dashboard and you must paste the new value here, or both charging and confirmations stop at once. Yes
Currencies (coma separated) Defaults to NGN,GHS,ZAR,KES. Trim it to what your Paystack account is actually enabled for. See the currency trap. No

One more Paystack setting lives at Recurring Payments → Settings → General: Paystack card-verification amount (minor units). Installation seeds it at 5000. Review it before you go live — see the next section but one, because what that number is worth depends entirely on your currency.

The webhook, and the Paystack special case

In the Paystack dashboard go to Settings → API Keys & Webhooks and set the webhook URL to the one from Diagnostics:

https://your-crm.example.com/autobill/autobill_webhook/paystack

We deliberately do not reproduce the Paystack dashboard here. Paystack redesigns it periodically, and a screenshot that no longer matches what you are looking at is worse than none — the URL above and the settings below are what actually matter, and they do not change.

There is no separate webhook secret to copy, and that is correct. Unlike Stripe and GoCardless, Paystack does not issue a distinct signing secret: it signs each delivery with a keyed hash (HMAC-SHA512) of the raw request body using your secret key, sent in the x-paystack-signature header. The module verifies every delivery that way. This is why the Paystack block on the Diagnostics screen shows no Webhook signing secret row at all — there is nothing to be missing. If your secret key is correct, signature verification is correct.

Paystack keeps separate test and live webhook URLs in the dashboard; set both, and remember that switching the secret key from test to live switches signature verification with it.

The module acts on these events and answers 200 to everything else:

EventWhat the module does with it
charge.successTwo jobs. If it is the verification charge from a card being saved, it completes the setup (the fallback for a customer who closed the tab). Otherwise it writes the payment into Perfex against the invoice.
charge.failedMarks the attempt failed with Paystack's gateway response and starts dunning.
invoice.payment_failedHandled exactly like charge.failed.

Paystack does not send an event id of its own, so the module builds a stable de-duplication key from the event type and the transaction identity. Redelivering the same event is therefore still safe.

How a customer saves a card, and the verification charge

Paystack has no setup-only flow. There is no equivalent of a card being validated without a payment: a reusable authorisation only comes back from a real transaction. So when a customer adds a card from the portal without an invoice attached, the module runs a small real charge on their card to capture the authorisation.

You control the amount with Paystack card-verification amount (minor units) under Recurring Payments → Settings → General. It is in minor units, and it is seeded at 5000 when the module is installed — that is 50.00 NGN, but the same 5000 is 50.00 in every other currency too, which may be far from trivial. Check this number against the currency you actually collect in before you go live. Setting it to zero switches card-saving off: the customer gets a clear message instead of a card being stored.

Be honest with your customers about this charge. It is a real payment on a real card, it appears on their statement, and this module does not refund it automatically. Choose an amount small enough to be trivial, tell your customers what it is for in your terms, and decide your own policy on refunding or crediting it. If a small unavoidable charge is not acceptable in your business, ask customers to save their card by paying a real invoice instead — that path stores the authorisation with no extra charge at all.

The flow itself: Billing & Cards → Add payment method → Credit / debit card, tick the authorisation, and Paystack's own hosted checkout takes the card details. On return the module verifies the transaction directly with Paystack, checks that the resulting authorisation is marked reusable, and stores the authorisation code, the card's cosmetic details and the e-mail the authorisation belongs to. Card details never touch your server.

If the authorisation comes back not reusable — which is what happens on non-card channels — the module refuses to store it and says so, rather than saving a token that would fail on the first automatic charge.

One end-to-end test

  1. Enter your sk_test_ and pk_test_ keys, set Active to Yes, check the currency list. Save.
  2. Check the card-verification amount and adjust it to something trivial in your test currency. Save.
  3. Point Paystack's test webhook URL at your endpoint.
  4. Diagnostics should show Paystack Enabled and credentials Set. There is deliberately no webhook-secret row.
  5. As a test customer, add a card. Use Paystack's published test card (4084 0840 8408 4081, CVV 408, any future expiry, PIN 0000, OTP 123456) — check Paystack's testing documentation for the current values, they are Paystack's, not ours. The verification charge runs and the card is stored.
  6. Press Charge now on that customer's subscription and watch Charge Attempts: the row should reach Succeeded with a Paystack reference, and the invoice should be paid.

Paystack behaviour you must understand

The token is bound to the e-mail that created it. Gateway's own product: a Paystack authorisation belongs to the customer record identified by the e-mail address used when the card was saved. Charging it with a different e-mail fails. The module therefore stores that original e-mail alongside the token and always charges with it, even if the contact later changes their address in Perfex. That is intentional: guessing the new address would produce a decline. If a customer changes e-mail and you want the two to match at Paystack, have them add their card again from the portal and remove the old method.

Reusable authorisations only. The setup flow is restricted to the card channel, because that is the only channel that returns something chargeable again later. Bank transfer, USSD and mobile money can pay an invoice interactively but cannot be stored for automatic collection.

Two-factor prompts and paused authorisations. Some cards demand a PIN, an OTP or a hosted confirmation step even on a repeat charge, and Paystack can also report an authorisation as paused. The module cannot answer any of these — only the cardholder can — so instead of retrying blindly it marks the attempt Requires Authentication and e-mails the customer a single-use link that takes them to Paystack's own confirmation page for that transaction. The link is validated before use and only ever leads to a Paystack domain. As with Stripe, it expires after the Authentication link lifetime (hours) you configured.

Duplicate references are resolved, not re-charged. Each attempt uses a deterministic reference derived from the invoice and attempt number. If Paystack reports that reference as already used, the module asks Paystack what happened to the original transaction rather than sending a second charge under a new reference. This is the guard against double-charging a customer when a request times out.

Reading Charge Attempts

Recurring Payments → Charge Attempts is the log every test and every incident ends at. Each row is one attempt on one invoice, with the gateway's own transaction id underneath the invoice number, the decline and advice codes the gateway returned, the raw error message, and the next scheduled retry. Filter by status and gateway at the top.

StatusMeaning
PendingOpened, waiting on the gateway. If it sticks here for more than an hour, cron re-drives it under the same idempotency key — it cannot double-charge.
ProcessingThe gateway accepted it and has not finished.
In TransitGoCardless only. Submitted to the bank; days may pass. Not a problem.
SucceededMoney recorded against the invoice in Perfex.
FailedDeclined. The decline code and message are shown, and dunning has taken over.
Requires AuthenticationThe customer must confirm on-session. A recovery link has been e-mailed to them.
Charged BackThe payment was disputed or clawed back. Staff have been notified.
CancelledThe attempt was abandoned before any money moved.

Retry now appears on Failed and Requires Authentication rows for staff with the manual-charge permission. It re-queues the attempt through the normal engine, which means the safety cap, the currency check and the idempotency key all still apply. It never charges the card directly.

Late fees and suspension

Two things added in version 1.1 sit next to the gateways. Neither is on when you install: late fees arrive with their master switch off, and no dunning policy suspends anything until you choose one of the two suspending terminal actions on it. Neither has gateway-specific behaviour of its own either — no gateway is named anywhere in the late-fee code, and a suspension is the same on all three rails. How a GoCardless subscription reaches a suspension is not the same, and the last section here says why.

A late fee is a separate invoice, and nothing charges it

A fee accrues against the overdue invoice, in a ledger of the module's own — one row per invoice — and never as a line on the invoice itself. When it is turned into a document it becomes its own Perfex invoice, with its own number, in the same currency as the original. The original is never written to — not its lines, not its total, not its status — so the PDF the customer already holds stays correct. Deleting the fee invoice leaves the original untouched; deleting the original removes the accrual with it.

Note the scope, because it is wider than this page's subject: the sweep picks candidates on an invoice's own status and due date, with no reference to subscriptions at all. Once the feature is on it accrues against every overdue or partially paid invoice in your Perfex install, not only the ones a gateway on this page charges.

Nothing on this page charges a fee invoice automatically. Every automatic collection described here runs off a subscription: the due sweep reads schedules, and for an adopted recurring invoice it only looks at the invoices Perfex generated from that one. A fee invoice belongs to no schedule and is not one of those, so no sweep opens a charge attempt against it and no stored card is debited for it on its own. Charge now does not reach it either — that button charges the subscription's own open invoice.

A fee invoice is raised with the same payment modes your own invoice form would offer. Perfex treats an invoice with an empty list as offering no payment option at all, so the module resolves the list explicitly rather than leaving it blank — an unattended document nobody can pay is worse than no document. If one of those modes is a gateway from this page, the customer pays it from the portal in the ordinary way and this module handles that payment like any other — what it never does is take the money without being asked.

Suspension stops collection

Two of the dunning terminal actions — Suspend the subscription and Suspend the subscription, then terminate it — move the schedule to suspended. Two things stop at that point: the due sweep no longer serves it, because it only reads schedules that are active or trialing, and applying a terminal action clears the next-retry date on every attempt against that invoice, so its retry ladder stops as well. A suspended subscription is neither billed for a new period by the sweep nor retried on the invoice that suspended it.

Two things it does not stop, and both are deliberate. Charge now still works while a subscription is suspended, so staff can take a payment the moment a customer calls in. And if the subscription rides on a Perfex recurring invoice rather than on a plan, Perfex's own recurring-invoice generator is not ours to stop — suspend one of those and stop or pause its recurring invoice in Perfex as well.

Suspension stops billing. It does not touch the customer's login. The module does not switch off the Perfex contact, which is the same refusal version 1.0 made: that login is the one place the customer can replace the card that failed, and locking them out of it makes the debt harder to collect, not easier. Whether a suspended customer keeps your actual service is your decision, taken outside this module.

A suspended subscription comes back on its own; a terminated one does not. When the invoice that caused the suspension is settled in full — by any channel, including a payment your staff enter by hand — the schedule returns to active, or to needs payment method if the stored card was removed while it was away. A part payment is not a recovery and lifts nothing. Staff can also lift a suspension from the subscription screen. Termination is final: a payment against a terminated subscription notifies your staff and changes no status.

Pick the terminal action by its behaviour, not by its name. The dunning policy editor also offers an older action labelled Suspend portal access (aggressive). Despite the word “suspend” it does not produce the suspended status described here: it cancels the subscription and notifies your staff, and a cancellation is not something a payment undoes. The two actions that suspend are the two named above.

GoCardless and the terminal action

None of this changes what the GoCardless section says: the module never retries a bank debit, and version 1.1 has not altered that. It does mean the terminal action is reached differently on that rail. On Stripe and Paystack it fires when the retry ladder runs out, or on the Give up after (days) day of the dunning policy, whichever comes first. On GoCardless there is no ladder to run out, so Give up after (days), counted from the first failure, is the only thing that fires it. Check that figure before you point a suspending policy at GoCardless customers.

Troubleshooting

SymptomMost likely cause
The gateway does not appear on an invoice at all. The invoice's currency is not in the gateway's currency list, or Active is No. Check Diagnostics.
The customer paid, but the invoice is still unpaid. The webhook is not arriving or not verifying. Look at the gateway's own delivery log: 400 means a wrong signing secret, 503 means the module is deactivated or has no credentials, a timeout means your site is not reachable.
Opening the webhook URL in a browser returns 405. Correct. The endpoint only accepts POST.
Charge now says No charge was made. The engine refused before contacting the gateway: currency not enabled, gateway not configured, or the method hit the cap of 8 attempts in 14 days.
A GoCardless attempt has been In Transit for days. Expected. Bank debit settles over several working days.
A failed GoCardless attempt shows no next retry. Expected. GoCardless owns bank-debit retries; retrying here as well would collect twice.
A late-fee invoice sits unpaid, and the customer says the portal offers no way to pay it. A fee invoice is raised with the same payment modes your own invoice form would offer, so an empty list means no payment mode on your install is enabled for anything but expenses. Check Setup → Payment Modes. Separately, no charge is ever attempted on a fee invoice automatically — that part is by design, because it belongs to no subscription.
A suspended subscription shows no next retry on its failed attempt. Expected. The terminal action clears the next-retry date on every attempt for that invoice before it suspends.
A 3-D Secure confirmation link dead-ends. The Stripe publishable key is empty, or the link has passed its lifetime. Both are fixable in settings; then use Retry now to issue a fresh link.
Adding a Paystack card fails immediately. The card-verification amount is set to zero. Give it a real value in the module settings.
Everything is configured but nothing charges on schedule. Perfex cron is not running. The Diagnostics engine self-check shows when it last ran.
Unprocessed webhooks keeps climbing. Events are arriving but failing to process. Cron replays them automatically; if the number does not fall, staff are notified once the replay limit is reached.