Accounts Controllers
The cartridge extends SFRA's Account, Address, and PaymentInstruments controllers with server.prepend/server.append, and each extension calls the same forterCustomerUpdate.js pipelet used across every account event. Response actions for each outcome are configured on Accounts Configuration.
Extended routes
Customer action | Route | Extension | Forter call | On decline |
|---|---|---|---|---|
Log in | Account-Login | prepend | login | Logs the customer back out and returns the decline message. |
Sign up | Account-SubmitRegistration | prepend + append | signup | Deletes the account SFRA just created and returns the decline message. |
Update profile (name, email, phone) | Account-SaveProfile | prepend + append | profile-access (email/phone only) | Reverts the changed fields and returns the decline message. |
Change password | Account-SavePassword | append | profile-access | Reverts the password and returns the decline message. |
Add or edit an address | Address-SaveAddress | prepend | profile-access | Blocks the save and returns the decline message. |
Remove an address | Address-DeleteAddress | prepend | profile-access | Blocks the delete (403) and returns the decline message. |
Add a payment method | PaymentInstruments-SavePayment | prepend | profile-access | Blocks the save and returns the decline message. |
Remove a payment method | PaymentInstruments-DeletePayment | prepend | profile-access | Blocks the delete (403) and returns the decline message. |
Profile-access calls only fire for the fields that actually changed. Editing just a phone number doesn't trigger an email check, and saving a profile with no email/phone change doesn't call Forter at all.
On a VERIFICATION_REQUIRED decision, Login and every profile-access caller also report an authentication-result back to Forter (forterAuthenticationAttemptUpdate.js), tied to the decision's correlationId. See Accounts Configuration for what you still need to build to act on a Verification Required outcome.
Signup declines reverse account creation
SFRA's base SubmitRegistration creates the customer account before Forter's appended handler runs. On decline, the handler removes that same account (CustomerMgr.removeCustomer) rather than preventing its creation up front. The customer briefly exists mid-request and is gone by the time the decline response is returned.
Two different decline mechanisms
SaveProfile and SavePassword revert an already-committed change. Both use server.append, so SFRA's base logic runs and commits the new value first; Forter's handler captures the previous value going in and, on decline, restores it. SavePassword's implementation says this directly: "Base commits the new password inside its own BeforeComplete... so if Forter declines we need the old password to roll the change back."
SaveAddress, DeleteAddress, and DeletePayment never let the change happen in the first place. All three are server.prepend hooks that check Forter before calling next(). On decline, the base save/delete logic never runs, so there's nothing to roll back.
Either way, the customer sees the decline message and their account ends up exactly as it was before the request.
Which declines render automatically, and which need a frontend change
Login, SubmitRegistration, SaveProfile, SavePassword, SaveAddress, and SavePayment all return { success: false, error: [...] }. Base SFRA's client-side formValidation() helper, already wired into each of these forms' AJAX success handlers, renders that error array as an alert banner automatically. No frontend change needed.
DeleteAddress also renders automatically, through a different path: its decline returns HTTP 403, which base SFRA's addressBook.js AJAX error handler catches and displays via createErrorNotification(err.responseJSON.errorMessage).
DeletePayment is the exception. Its decline also returns HTTP 403 with errorMessage, but base SFRA's paymentInstruments.js error handler for removing a payment method only checks for a redirectUrl — there's no fallback branch that displays errorMessage. A Forter decline here fails silently unless you add one. Override removePayment in a cartridge ahead of app_storefront_base and add the missing branch:
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else if (err.responseJSON.errorMessage) {
// display err.responseJSON.errorMessage to the customer
}
$.spinner().stop();
}Payment methods: SavePayment fully replaces the base route
Unlike every other row in the table above, PaymentInstruments-SavePayment's prepend never calls next(). It reimplements card validation and instrument creation itself and always terminates the route directly, so SFRA's base SavePayment logic never runs.
This matters for PSP cartridges that also take over payment-saving. If a PSP tokenizes cards through its own route, bypassing PaymentInstruments-SavePayment entirely, Forter's controller hook never fires. Call the app.payment.saved hook from the PSP's own flow instead:
hooksHelper('app.payment.saved', 'paymentSaved', paymentData, ...);See Example: Stripe Payment Element for this applied in detail, and PSP Integration Guide for the equivalent pattern on the order side (app.post.auth).
If a PSP cartridge instead prepends PaymentInstruments-SavePayment directly rather than using its own route, both prepends compete for the same unconditional termination. Only the one earlier in the cartridge path runs, and it's silent either way. Placing int_forter_sfra before any PSP cartridge (see Import Cartridge) decides that order, but doesn't eliminate the risk; if the PSP cartridge wins, add a call to the app.payment.saved hook from its flow so Forter still sees the update.