Stripe for OpenCart 4.1, integrating with the Stripe API: the Stripe Payment Element on the Payment Intents API with automatic payment methods (cards, Apple Pay, Google Pay, Link and the local methods switched on in the Stripe Dashboard such as iDEAL, Bancontact, BLIK, SEPA Direct Debit and Klarna), 3D Secure and SCA by Stripe, charge at once or authorize and capture later (by order status or by hand, full or lower amount), cancel authorizations, full and partial refunds from the order page, bank payments that take days with a waiting page and their own status, redirect methods through a SameSite-safe return page, signed webhooks (Stripe-Signature, raw body, 5-minute tolerance). A payment counts as paid only after the shop read it back from Stripe and it matches the current order total; every POST carries an idempotency key stored before it is sent; restricted API keys only, as Stripe requires. Events and cron, no OCMOD.
Tested on 4.1.0.4Updated Nov 8, 2026Version 1.0.0Compatible with OpenCart 4.1.0.4 No core file edits
Built forOpenCart
One form, every way to pay1 of 5
What it does
Stripe for OpenCart 4.1, integrating with the Stripe API: the Stripe Payment Element on the Payment Intents API with automatic payment methods (cards, Apple Pay, Google Pay, Link and the local methods switched on in the Stripe Dashboard such as iDEAL, Bancontact, BLIK, SEPA Direct Debit and Klarna), 3D Secure and SCA by Stripe, charge at once or authorize and capture later (by order status or by hand, full or lower amount), cancel authorizations, full and partial refunds from the order page, bank payments that take days with a waiting page and their own status, redirect methods through a SameSite-safe return page, signed webhooks (Stripe-Signature, raw body, 5-minute tolerance). A payment counts as paid only after the shop read it back from Stripe and it matches the current order total; every POST carries an idempotency key stored before it is sent; restricted API keys only, as Stripe requires. Events and cron, no OCMOD.
What it changes for your store
Stripe's Payment Element, built for OpenCart 4.1. One form that shows each customer the payment methods that fit their country, currency and amount: cards, Apple Pay, Google Pay, Link and the local methods you switch on in your Stripe Dashboard. Stripe handles 3D Secure and Strong Customer Authentication. Your own Stripe account, Stripe's own form, nothing in between.
Know the limits. 1.0 has no saved cards, subscriptions or Express Checkout buttons on product and cart pages, and does not offer voucher methods paid days later (Multibanco, OXXO, Boleto, Konbini). You need your own Stripe account; Stripe charges its own fees. Tested against a stand-in built from Stripe's published API contract, not against a live Stripe account; the documentation lists what to confirm with your own test keys. Stripe is a trademark of Stripe, Inc.; this extension is independent software that integrates with the Stripe API and is not affiliated with or endorsed by Stripe.
Features
Checkout
Stripe Payment Element (Stripe.js endive) in accordion or tabs layout, Stripe / Flat / Night style and your accent colour
Card and bank details are typed into Stripe's iframes and never reach your server
Redirect methods (iDEAL, Bancontact, BLIK, Klarna ...) come back to the success page with the customer's session intact on OpenCart 4.1's strict cookies
Bank debits that take days (SEPA Direct Debit) wait in a "Processing at Stripe" status with a waiting page; never shown as cancelled
Customer details prefilled; decline messages from Stripe shown as they are
Safe payments
Paid means paid: an order becomes paid only after the shop read the payment back from Stripe and its amount still equals the order total
A total that changed before Pay updates the payment first; a payment that still does not fit is kept and flagged for you to accept, refund or cancel; a second payment of a paid order is refunded automatically
Every request that changes money carries an idempotency key stored before it is sent; lost answers are reconciled, never sent twice
Restricted API keys only, as Stripe requires for third-party integrations; keys and webhook secret stored encrypted
After the order
Charge at once or authorize and capture later (on Shipped, or by hand, full or a lower amount); cancel authorizations
Full and partial refunds with a Stripe reason, from a Stripe tab on the order page with a link to the Dashboard
Signed webhooks (Stripe-Signature, raw body, 5-minute tolerance) confirm bank payments, disputes and Dashboard refunds
Test and live keys side by side; connection test; one-click domain registration for Apple Pay, Google Pay and Link
Test mode stays hidden from customers until you confirm it
Updates and uninstall keep payment records, settings and keys; "Remove all data" when you really want it
What you get
Download & installOne installer package (.ocmod.zip) for OpenCart 4.1.0.0 – 4.1.x. There is no OpenCart 3 edition. Download it from My licenses right after payment.
12 months of updates & supportNew versions and help for 12 months (24 or 36 months at checkout). After that the extension keeps working; renew anytime at 50%.
License for your storeOne production domain with all its subdomains; staging and local development hosts are free. How licensing works
30-day refundFull refund within 30 days of your first purchase, no reason needed. Refund policy
Tested buildsOpenCart 4.1.0.4. Tested on OpenCart 4.1.0.4.
Documentation & supportFull guide (Installation, Configuration, Usage, Developer notes) on this page. Tickets from your account, first reply within 1 business day.
Compatibility
Payments for Stripe (OpenCart 4) works with OpenCart 4.1.0.4.
We list the builds we install and test on. “Tested” means the test plan ran on that build; “Supported” means the build is inside the supported range.
Compatibility
Version
Range
Runs on
Status
OpenCart 4.1.0.x
4.1.0.0 – 4.1.0.4
PHP 8.1 – 8.3
Tested on 4.1.0.4
1.0.0
Nov 8, 2026
Latest
First release: Stripe for OpenCart 4.1 with the Stripe Payment Element on the Payment Intents API (automatic payment methods: cards, Apple Pay, Google Pay, Link and the local methods switched on in the Stripe Dashboard), 3D Secure / SCA by Stripe.js, charge at once or authorize and capture later (by status or by hand, full or lower amount), cancel authorizations, full and partial refunds from the order page, processing payments (bank debits) with a waiting page and a "Processing at Stripe" status, redirect methods through a SameSite-safe return page, signed webhooks (Stripe-Signature, raw body, 5-minute tolerance) on a parameter-free endpoint, idempotency key on every POST with reconciliation of unclear answers, amount checks against the current order total, restricted API keys only (as Stripe requires for third-party integrations), sealed keys, pinned API version 2026-10-28.endive, hourly cron, data kept across updates.
Capture, void or refund started by an order status you set on the admin order page writes the order history through the OpenCart order model: the customer gets the status e-mail (when Notify customer is on) and other extensions see the change.
How do I install the extension?
Upload. Extensions > Installer: upload the .ocmod.zip. It is for OpenCart 4 (4.1.0.0 to 4.1.x).
Install and enable. Extensions > Extensions > Modules: install, open the settings and switch it on. Events register themselves; nothing is patched.
Enter the license key. Paste the key from My licenses. Subdomains, staging and local hosts are included; you have 14 days to do it.
OpenCart 4.1.0.4. Tested on OpenCart 4.1.0.4. The Compatibility tab lists every version range.
Do the extensions change core files?
No. They work through OpenCart events and standard extension types (modules, order totals, themes). No core file edits, no OCMOD patches, no theme template edits, so OpenCart updates and other extensions keep working.
What does the license cover?
One production store domain with every subdomain (www, shop., staging.), plus local development hosts such as localhost, *.local and *.test. A store on another domain needs its own license; a second-store license is 40% off. You have 14 days after installing to enter the key. 12 months of updates and support are included; after that the extension keeps working and renewals are 50% off. How licensing works
Can I get a refund?
Yes. Ask for a full refund within 30 days of your first purchase, no reason needed: open a ticket or write to [email protected] with your order number. The money goes back to the card or bank account you paid with within 5 business days of approval. Refund policy
How do I get help?
Open a ticket from your account, so your license details are attached. We answer Monday to Friday: bug reports within 1 business day, how-to questions within 2. Support policy. Not a customer yet? Ask a pre-sales question.
Take card, wallet and local payments through your own Stripe account on OpenCart 4.1 with the Stripe Payment Element on the Payment Intents API. Stripe shows each customer the payment methods that fit their country, currency and amount (cards, Apple Pay, Google Pay, Link and the local methods you switch on in your Stripe Dashboard, such as iDEAL, Bancontact, BLIK, EPS, Przelewy24, SEPA Direct Debit or Klarna), and runs 3D Secure / Strong Customer Authentication when the bank asks for it. Charge at once or authorize and capture later; cancel authorizations, full and partial refunds from the order page.
What it does
One modern form. The Payment Element is loaded from js.stripe.com (version endive, the same release train as the pinned API version 2026-10-28.endive). Card and bank details are typed into Stripe's own iframes and never reach your server. Accordion or tabs layout, Stripe / Flat / Night style and your accent colour.
Restricted keys, as Stripe requires. Stripe asks third-party integrations to use restricted API keys (rk_) instead of secret keys. The extension accepts only restricted keys and tells you exactly which permissions to give it. The key and the webhook signing secret are stored encrypted.
Paid means paid. An order moves to your paid status only when the shop read the PaymentIntent back from Stripe, its amount, currency and reference are the ones the shop sent, and the order still costs that amount. If the cart changed while the customer paid (or the order was edited while a bank payment was processing), the money is kept and the payment is flagged on the order, never marked paid: you accept it, refund it or cancel the authorization with one click. Only a second payment of an order that is already paid is refunded automatically. A total that changed before the customer pressed Pay updates the PaymentIntent first; the customer sees the new amount.
Bank payments that take days. Methods such as SEPA Direct Debit stay "processing" at Stripe for days. The order waits in your "Processing at Stripe" status (default Pending) with a "Do not ship yet" note, the customer sees a waiting page, and the order is marked paid when Stripe confirms. It is never shown as cancelled while Stripe is still processing it.
Redirect methods on OpenCart 4.1. OpenCart 4.1 sends its session cookie with SameSite=Strict, so a customer who returns from a bank page would lose the session. Stripe returns the customer to a small bounce page (extension/kv_stripe/return.php) that continues inside the shop, so the success page, the emptied cart and a signed-in account all keep working.
Never twice. Every create, update, capture, cancel and refund carries an idempotency key that is stored before the request. A lost answer is sent again with the same key, or reconciled by reading the payment back (Stripe caches a 5xx answer for its key); nothing is sent twice.
Signed webhooks. Events arrive on a parameter-free endpoint and are checked with their Stripe-Signature on the raw body with a 5-minute tolerance; each event is applied once and only triggers a read-back from Stripe.
Updates keep your data. OpenCart 4 updates are uninstall + install: payment records, settings and keys survive.
Requirements
OpenCart 4.1.0.x, PHP 7.4 to 8.3 with cURL and OpenSSL, HTTPS on the live shop.
Your own Stripe account. Stripe's fees apply.
Not in 1.0
Saved cards and subscriptions, Express Checkout buttons on the product or cart page, Stripe Tax, multicapture. Voucher and cash methods that customers pay days later (Multibanco, OXXO, Boleto, Konbini) are not offered. OpenCart 3 is not supported (no OpenCart 3 package is shipped).
Stripe is a trademark of Stripe, Inc. This extension is independent software by Kavio Labs that integrates with the Stripe API; it is not affiliated with or endorsed by Stripe.
Installation
Install the extension. Extensions > Installer > Upload kv_stripe.ocmod.zip, then Install.
Install the payment method. Extensions > Extensions > Payments > Payments for Stripe > Install, then Edit.
Keys (test mode first). In the Stripe Dashboard switch to test mode (sandbox) and open Developers > API keys.
Copy the publishable key (pk_test_...).
Click Create restricted key and give it Payment Intents: Write, Charges and Refunds: Write and, if you want the domain button, Payment Method Domains: Write. Everything else stays None. Copy the key (rk_test_...).
Secret keys (sk_...) are refused: Stripe requires third-party integrations to use restricted keys.
Enter both in the "Test mode keys" block on the Connection tab and press Save and test connection.
Webhook. Webhooks, log and cron tab: copy the webhook endpoint URL (https://your-shop/extension/kv_stripe/notify.php). In the Dashboard open Developers > Webhooks > Add endpoint, paste the URL and select the events listed on the tab (payment_intent.succeeded, payment_intent.processing, payment_intent.payment_failed, payment_intent.amount_capturable_updated, payment_intent.canceled, charge.refunded, charge.refund.updated, charge.dispute.created, charge.dispute.updated, charge.dispute.closed). Copy the endpoint's signing secret (whsec_...) into the test block.
Payment methods. In the Dashboard open Settings > Payment methods and switch on the methods you want. The Payment Element shows each customer the ones that fit.
Payment tab: switch the method on; choose the layout, the style and your accent colour.
Capture and statuses tab: charge at once (recommended) or authorize only; when to capture; the order statuses.
Place a test order with a Stripe test card (4242 4242 4242 4242, any future date, any CVC). Test mode is hidden from your customers until you tick "I confirm: show the payment method to real customers in test mode" on the Connection tab; tick it only on a test shop. Test orders get the note "TEST MODE (Stripe sandbox): no money moved" and no e-mail.
Go live. Create a live restricted key with the same permissions and a live webhook endpoint, enter the live keys, switch the mode to Live and test the connection again. On the live shop press Register your-domain with Stripe (or add the domain under Settings > Payment method domains) so Apple Pay, Google Pay and Link appear.
Cron. Make sure the OpenCart cron runs (*/10 * * * * php /path/to/cron.php). The extension adds an hourly job.
Updating
Uninstall the payment method (Extensions > Payments > Uninstall), uninstall and delete the old files in the Installer, upload the new zip, install, install the payment method. Payment records, operations, the log, your settings and keys are kept. Open the settings page once afterwards.
Removing all data
Settings > Webhooks, log and cron > Remove all data (type DELETE) deletes the payment records, the log, the keys and the kept settings. It is refused while a payment is processing or unclear or an authorization is not captured or cancelled. The licence stays.
Configuration
Setting
Default
What it does
Status
off
Offers the method at checkout (only with both keys of the selected mode and a currency Stripe can charge)
Name at checkout
"Card, wallet or bank payment"
Payment method name
Form layout
Accordion
Payment Element layout: accordion or tabs
Form style / accent
Stripe, theme colour
Appearance API theme (stripe, flat, night) and colorPrimary
Reference prefix
empty
Description "prefix Order #1042"; metadata order_id and kv_ref on every PaymentIntent
Geo zone, sort order
all, empty
Standard OpenCart payment options
Mode
Live
Test or Live keys
I confirm: show ... in test mode
off
Without it the method is hidden while the mode is Test
Publishable key
Per mode, pk_test_ / pk_live_
Restricted key
Per mode, rk_test_ / rk_live_, stored encrypted (AES-256-GCM); sk_ refused
Webhook signing secret
Per mode, whsec_, stored encrypted
Capture
Charge when the customer pays
capture_method automatic_async, or manual (authorize only; methods that cannot be captured later are not offered)
Capture an authorization
at the capture status
At the capture status or a complete status, or only by hand
Capture at status
Shipped
Cancel authorization at status
Canceled
Cancels an authorization that is not captured yet; charged payments are never refunded automatically
Charged / Authorized
Processing / Pending
Processing at Stripe
Pending
While Stripe is still processing (bank debits); do not ship
Failed after processing
Failed
Only while the order is still in the processing status
Captured / Authorization cancelled / Fully refunded / Dispute opened
Processing / no change / Refunded / no change
E-mails
order e-mail on, Stripe receipt off
OpenCart status e-mail when paid (live mode); optional Stripe receipt (receipt_email, live mode)
Debug log
off
Logs every request and response (keys, client secrets, signatures and personal data masked)
Usage
At checkout
The customer chooses "Card, wallet or bank payment". The confirm step loads the Stripe Payment Element with the methods that fit the order, and a Pay amount currency button. On Pay the shop first checks that the order still costs what the form shows (if not, the amount is updated and the customer is asked to press Pay again), then Stripe confirms the payment, with 3D Secure when the bank asks for it.
Cards and wallets: the customer lands on the success page after the shop has read the payment back from Stripe.
Redirect methods (iDEAL, Bancontact, BLIK, Klarna ...): the customer approves at the bank and comes back through the bounce page to the success page.
Bank debits (SEPA Direct Debit ...): Stripe processes them for days. The basket is emptied, the customer sees a waiting page, the order waits in "Processing at Stripe" and becomes paid when Stripe confirms. It is never shown as cancelled meanwhile.
Declines: Stripe's message ("Your card was declined.") appears; nothing is charged and the customer can try again on the same form.
On the order page
Sales > Orders > the order > Stripe tab:
Amount, status, PaymentIntent id with a link to the Stripe Dashboard, payment method (card brand and last four digits, wallet), 3D Secure result, dispute status, every operation.
Capture (authorizations): once, up to the authorized amount; a lower amount releases the rest.
Cancel authorization: the customer is not charged.
Refund: full or partial, several times, with an optional Stripe reason (requested by customer, duplicate, fraudulent).
Accept as paid (amount mismatch only): books the payment although it does not match the order total.
Read back: reads the payment from Stripe (also picks up refunds made in the Dashboard) and reconciles operations without a clear answer.
Automatic actions
Authorize only + capture at status: changing the order to Shipped (or a complete status) captures; Canceled cancels the authorization.
A payment that no longer matches the order total is kept and flagged ("Amount mismatch (money kept, not marked paid)"); the settings page shows a red notice and the Stripe tab offers Accept as paid, Refund and Cancel authorization. An authorization that does not match is never captured by an order status.
A second payment of an order that is already paid (or cancelled) is refunded or cancelled automatically and noted.
If a capture by order status cannot be done (Stripe busy, an earlier operation unclear), the order history says so with the date Stripe would cancel the authorization, and the cron tries again while the order stays in the capture status.
A refund that fails at the bank later is written to the order history ("the customer has NOT got this money back").
An authorization Stripe cancels after 7 days without capture gets a history note; after 5 days the order history gets a warning and the settings page counts it.
Unpaid attempts older than 48 hours are cancelled at Stripe (reason "abandoned"); the order is not touched.
Dispute events add a history note (and, if you chose one, the dispute status).
Developer notes
Item
Value
Extension code
kv_stripe (type payment)
Settings
payment_kv_stripe_* (restricted key and webhook signing secret sealed with Secret v2, AES-256-GCM, installation key in kv_seal)
Tables
kv_stripe_txn (one row per PaymentIntent), kv_stripe_op (captures, cancels, refunds), kv_stripe_hook (ids of handled events), kv_stripe_log
Events
admin/view/sale/order_info/before (order tab), admin/controller/common/header/before (licence tick), catalog/model/checkout/order.addHistory/after (capture / cancel on order status; also runs for admin status changes, which OpenCart 4.1 runs in-process through sale/order.call)
Cron
extension/kv_stripe/payment/kv_stripe.cron (hourly; also callable with the secret key; guard func_num_args() < 4 = OpenCart's cron runner, else non-empty key + hash_equals)
Routes (catalog)
.prepare (PaymentIntent + client secret), .verify (total check before confirming), .confirm (read-back after confirmPayment), .back (return of redirect methods, reached through extension/kv_stripe/return.php), .wait / .status (processing), .webhook (Stripe is given <shop>/extension/kv_stripe/notify.php), .statusEvent, .cron
Package files
notify.php (webhook, one fixed route), return.php (SameSite=Strict bounce, kv return-file)
Packages
kv_stripe.ocmod.zip (OpenCart 4.1.x only; oc3/ is the toolchain's do-nothing placeholder and no OpenCart 3 package is built)
Server API
Stripe REST API v1 over cURL, no SDK bundled (src/Api.php), pinned Stripe-Version: 2026-10-28.endive
Client
Stripe.js https://js.stripe.com/endive/stripe.js, Payment Element
Decisions
Payment Element with PaymentIntents, intent-first. "Create a PaymentIntent before collecting payment details" (accept-a-payment, payment-ui=elements&api-integration=paymentintents): the server creates the PaymentIntent, the browser initialises Elements with its client secret and confirms with stripe.confirmPayment. Stripe recommends Checkout Sessions for most new integrations; the brief asked for PaymentIntents, which keeps the OpenCart order total in charge of the amount.
No stripe-php. The official library is MIT licensed and supports our PHP range, but it is several MB, autoloads through Composer, and we use eight endpoints. Direct REST calls keep the package small; the one algorithm we reproduce (webhook signature) is documented step by step by Stripe.
Restricted keys only. "Migrate a plugin to Stripe Apps or Stripe Connect" (stripe-apps/plugins/decide-migration): "Starting September 30, 2024, all plugin developers must use secure authentication methods (OAuth 2.0, restricted API keys, Stripe Connect)"; for self-hosted back ends set up by hand: "Validate API Keys: Make sure businesses give you restricted API keys that start with the prefix with rk_, not sk_." Config::keyProblem() refuses sk_. Permissions (stripe-apps/reference/permissions): Payment Intents payment_intent_write, Charges and Refunds charge_write (refund create and list, latest charge read), optional Payment Method Domains payment_method_domain_write. A Stripe App (RAK install flow) is a possible later step and needs a Stripe account of ours.
Pinned version, same train in the browser. "Set Stripe's API version in your plugin" (building-plugins); "The current version is 2026-10-28.endive" (api/versioning); "keep the Stripe.js version and server-side API version on the same release train" (sdks/stripejs-versioning). Webhook payloads use the account's default version; the handler reads only id, type, livemode, data.object.object/id/payment_intent (and dispute status, reason, amount, currency) and reads the PaymentIntent back with the pinned version, so the account version does not matter.
Identification. building-plugins: without an official library "set the value of the User-Agent header on requests made to the Stripe API as name/version (url)" with the platform in the name: OpenCart KavioPaymentsForStripe/1.0.0 (https://kaviolabs.com); in the browser stripe.registerAppInfo({name, version, url}).
every API reference curl example (-d "automatic_payment_methods[enabled]=true")
Idempotency
Idempotency-Key: <32 hex> on every POST, stored before the request; never on GET
api/idempotent_requests: "all POST requests accept idempotency keys. Don't send idempotency keys in GET and DELETE requests"; "up to 255 characters"; "we suggest using V4 UUIDs, or another random string with enough entropy"
Errors
non-2xx with error.type, error.code, error.decline_code, error.message, error.param
api/errors
Retry rules
network error, 409 (not of type idempotency_error), 429: same key again, unless Stripe-Should-Retry: false; 5xx: outcome indeterminate and cached for the key, reconcile by reading back; a resend answered with a 4xx that is not a replay (no Idempotent-Replayed: true) means the first try never ran: the operation is failed; idempotency_error: never retried
error-low-level ("Retry with the same idempotency key and parameters within the first 24 hours"; "If a 409 indicates that another request with the same key is still executing, wait and retry with that key"; "Stripe caches 500 responses ... Retrying with the same key returns the cached response"; "Don't retry with a new idempotency key until you've resolved the original outcome"), error-codes (idempotency_key_in_use)
Window
operations resent with their key for 23 h, then reconciled only
api/idempotent_requests: keys removed "after they're at least 24 hours old"
Request id
Request-Id response header written to the log
api/request_ids
Calls
Call
Fields sent
Fields read
Source
GET /v1/payment_intents?limit=1 (connection test)
limit
object = list
api/payment_intents list
POST /v1/payment_intents
amount (Stripe minor unit), currency (lowercase), automatic_payment_methods[enabled]=true, capture_method (automatic_async or manual), description, `metadata[kv_ref
order_id
platform], excluded_payment_method_types[] = boleto, konbini, multibanco, oxxo (voucher methods waiting days in requires_action), optional receipt_email` (live mode only)
api/payment_intents/create ("automatic_async (Default) ... Recommended over capture_method=automatic"; "manual: Place a hold ... (Not all payment methods support this.)"; currency "in lowercase"; receipt_email "you send a receipt regardless of your email settings")
POST /v1/payment_intents/{id}
amount, currency
amount, currency, status
api/payment_intents/update; accept-a-payment "Fetch updates from the server" (elements.fetchUpdates)
GET /v1/payment_intents/{id}?expand[]=latest_charge
api/payment_intents/search (a miss is never taken as proof: "data is searchable in less than a minute ... up to an hour behind during outages"; "not available to merchants in India"); used for creates that stayed unclear (5xx, or a network error / 409 / 429 that the same-key resend did not settle), and an unpaid PaymentIntent the browser never received cannot be paid. A signed webhook for such a PaymentIntent is matched by metadata.kv_ref and read by id (error-low-level: "include a local identifier in its metadata so you can match an object received through a webhook")
POST /v1/payment_intents/{id}/capture
amount_to_capture (only when lower than amount_capturable)
status succeeded or processing, amount_received
api/payment_intents/capture ("Defaults to the full amount_capturable"; "Uncaptured PaymentIntents are cancelled a set number of days (7 by default) after their creation"); lifecycle ("attempting to capture the funds moves it to processing or succeeded")
succeeded -> captured / partial_refund / refunded; requires_capture -> authorized; processing -> pending (order in "Processing at Stripe", waiting page, never cancelled); canceled -> canceled; requires_payment_method, requires_confirmation, requires_action -> open (not paid; after processing an explicit failure), except requires_action with next_action.type*_display_details or display_bank_transfer_instructions -> pending (an offline payment the customer still makes; never cancelled; such methods are excluded at create anyway). An amount that differs from the order -> error / MISMATCH, money kept, admin "Accept as paid"; only a duplicate (second payment of a paid order) is reversed automatically. Source: api/payment_intents/object status, payments/paymentintents/lifecycle.
Browser (Stripe.js)
Call
Source
<script src="https://js.stripe.com/endive/stripe.js"> loaded once, from js.stripe.com only
sdks/stripejs-versioning; accept-a-payment: "Always load Stripe.js directly from js.stripe.com to remain PCI compliant"
js/elements_object/create_payment_element; accept-a-payment ("Use defaultValues to pass a customer email address")
elements.fetchUpdates() after the server updated the amount
accept-a-payment "Fetch updates from the server"
stripe.confirmPayment({elements, confirmParams: {return_url}, redirect: 'if_required'}); an error of type card_error or validation_error shows error.message
js/payment_intents/confirm_payment ("When a non-redirect based payment method is successfully confirmed, stripe.confirmPayment will resolve with a {paymentIntent} object")
return_url = <shop>/extension/kv_stripe/return.php?txn=&k=&language=; Stripe appends payment_intent, payment_intent_client_secret, redirect_status; only payment_intent and redirect_status are passed on, and only the server read-back decides
confirm_payment return_url ("You can also append your own query parameters"); accept-a-payment ("Inspect the status of the PaymentIntent")
Raw body from php://input, header HTTP_STRIPE_SIGNATURE
"Stripe requires the raw body of the request to perform signature verification"
Split the header on ,, then =; t = timestamp, every v1; ignore other schemes (v0)
Step 1; "ignore all schemes that aren't v1"
signed_payload = t . "." . body; HMAC-SHA256 keyed with the endpoint secret
Steps 2 and 3
Constant-time compare with every v1 (rolled secrets)
Step 4; "multiple signatures ... when you roll an endpoint's secret"
Tolerance 300 s, never 0
"default tolerance of 5 minutes"; "Don't use a tolerance value of 0"
One secret per endpoint and mode; livemode must match the mode whose secret verified
"If you use the same endpoint for both test and live API keys, the secret is different for each one"
Duplicates by event id; order independent; objects read back
"Handle duplicate events", "Event ordering": "use the API to retrieve any missing objects"
Non-2xx is retried (live: up to three days); a failed read-back answers 503 and releases the event id
"Automatic retries"
Money (Money)
ISO 4217 exponents from tooling/release-templates/cscart/iso4217/gen.py (checked with --check); Stripe "amount" is the minor unit as Stripe defines it (currencies: "Currencies are two-decimal currencies unless otherwise specified"; zero-decimal list BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF; special case ISK "represent it as a two-decimal value, where the decimal amount is always 00"). Factor 1 where ISO and Stripe agree, 100 for ISK, refused where they disagree without a rule (MGA) or the page contradicts itself (UGX is on the zero-decimal list and in the special cases as two-decimal). Presentment currencies: union of the per-country lists on the currencies page (BHD, JOD, KWD, OMR, TND are not listed). Minimum amounts are left to Stripe (amount_too_small -> the customer is told).
OpenCart specifics
OpenCart 4.1 sends the session cookie with SameSite=Strict (system/config/default.phpsession_samesite), so the bank return goes through return.php (no OpenCart boot, no cookie, parameter whitelist) and SameSite::bounce() as a second line.
Order history through model_checkout_order->addHistory (catalog) or the store instance in the admin (createStoreInstance), so mails and stock behave like core payments.
Uninstall keeps data (Install::drop($purge = false), Keep snapshot restored by install()), as OpenCart 4 updates are uninstall + install.
Tests
tests/unit.php (PHP 8.1 and 8.3, 97 checks) against tests/mock-server/server.js (follows the sources above; never talks to Stripe) and a throw-away MariaDB database.
tests/e2e.js (53 checks) on an OpenCart 4.1.0.4 dev store; refuses to run unless the provider base is a loopback mock.
tooling/release-templates/oc-upgrade/upgrade-test.js oc_stripe <store>: data, settings and licence survive the OpenCart 4 update path.
Conformance with the Stripe documentation (OpenCart)
Checked against the Stripe API reference and guides listed in developer.md, read 2026-11-08.
How it was proven: unit tests against a local stand-in that follows those sources (PHP 8.1 and 8.3: money rules, webhook signatures with tolerance and rolled secrets, create / reuse / update / pay, declines, capture later, partial capture, refunds, cancel, processing payments, mismatches, duplicates, idempotent retries after a lost answer, a dropped connection, 409 and 429, Dashboard refunds, abandoned attempts), browser tests on OpenCart 4.1.0.4 against the same stand-in (settings and restricted keys, the Payment Element form, card, decline, changed total, redirect method through the SameSite bounce, processing payment with the waiting page and a signed webhook, forged webhooks, order tab refund / capture / cancel, cron) and the OpenCart 4 update path. It has not been run against a real Stripe account: no Stripe key of ours was used. Confirm the points below once with your own test-mode keys.
Topic
Documented behaviour
What the extension does
Auth
Basic auth, key as user name
Restricted key only (rk_), sk_ refused
Version
Stripe-Version header
2026-10-28.endive on every request; Stripe.js endive
Idempotency
Key on POST, 24 h, cached results incl. 500
Key stored before every POST; resend with the same key only when nothing started; 5xx reconciled by read-back
As documented, redirect if_required, amount re-checked first
Payment methods
automatic_payment_methods from the Dashboard
Enabled on every PaymentIntent; voucher methods (Multibanco, OXXO, Boleto, Konbini) excluded
3D Secure / SCA
Handled by Stripe.js during confirmPayment
Not intercepted; result recorded from the charge
Capture later
capture_method manual, amount_to_capture, 7 days
As documented; cron warning after 5 days
Refunds
Partial, several times, by payment_intent
As documented, metadata kv_op for reconciliation
Webhooks
Stripe-Signature v1, raw body, tolerance
As documented, 300 s, event id dedupe, read-back only
Currencies
Minor units, zero-decimal list, ISK special case
As documented; MGA, UGX refused
Points to confirm with a real test-mode account
A restricted key with exactly Payment Intents Write + Charges and Refunds Write can create, update, read (with expand[]=latest_charge), search, capture, cancel and refund (a missing permission is shown as "The restricted key lacks a permission").
The Payment Element shows the Dashboard methods in accordion and tabs layout; wallets on a registered HTTPS domain.
A 3D Secure test card (4000 0027 6000 3184) and an iDEAL / Bancontact test redirect return through return.php to the success page.
SEPA Direct Debit test payment: processing, then succeeded by webhook.
The webhook endpoint receives the listed events and "Send test event" from the Dashboard is answered 200 ("Ignored" for an unknown payment).
FAQ
Why a restricted key and not my secret key?
Stripe's "Migrate a plugin to Stripe Apps or Stripe Connect" guide: since 30 September 2024 third-party plugins must use secure authentication; for self-hosted integrations set up by hand: "Make sure businesses give you restricted API keys that start with the prefix rk_, not sk_". A restricted key can do only what you allow (here: payments and refunds), which limits the damage if it ever leaked.
Which payment methods?
Those you switch on in the Stripe Dashboard and that fit the order (country, currency, amount). With "Authorize only" Stripe offers only methods that support capturing later.
Multibanco, OXXO, Boleto, Konbini?
Not in 1.0. These voucher and cash methods wait days for the customer to pay offline; they are excluded from the Payment Element (excluded_payment_method_types) even if you switch them on in Stripe.
What happens if the order total changes while the customer pays?
Before Pay the payment is updated and the customer sees the new amount. If it still differs afterwards, the money is kept and flagged on the order; you accept, refund or cancel it on the Stripe tab. It is never marked paid by itself.
Do card numbers touch my server?
No. They are typed into Stripe's iframes; the shop only handles the PaymentIntent.
Which currencies?
The order's currency, as long as Stripe can present it (135 currencies; your account country decides the exact list). Amounts follow Stripe's rules: zero-decimal currencies such as JPY are sent as whole units, ISK as two decimals ending in 00. MGA and UGX are not offered because Stripe's and ISO 4217's decimals disagree for them; BHD, JOD, KWD, OMR and TND are not Stripe presentment currencies.
Do I need webhooks?
Strongly recommended: they confirm bank payments that take days and record disputes and Dashboard refunds. Card payments work without them (the shop reads every payment back), and the hourly cron reads processing payments back too.
Apple Pay and Google Pay do not appear. Register your live shop domain (Connection tab button, or Dashboard > Settings > Payment method domains) and use HTTPS. Wallets also need a supported browser and device.
Server-side tracking for OpenCart 4.x and 3.0.x: purchases, refunds, add to cart, checkout, product views and leads go from your server to the free official GA4 Measurement Protocol, Meta Conversions API and TikTok Events API, deduplicated with the browser pixels through shared event ids. Consent-aware, SHA-256 hashed customer data, order status mapping, queue with retries, test mode and an event log. No paid relay, events only, no OCMOD.
The department store theme for OpenCart 4.x and 3.0.x: colour-coded departments, a department list beside the hero slider with two side promos, a big search with a department selector, tabbed product blocks, a 2 + 1 + 2 offer grid, deals of the day with real countdowns, a department showcase, a promotions band and a three-column product page with a sticky buy box. Four styles (Meridian, Harbor, Citrus, Night) and your own colours on top. Events only, no core changes.
Stop AI crawlers, scrapers, vulnerability scanners and unwanted countries before OpenCart 4.x or 3.0.x builds a page. Googlebot and Bingbot are verified by DNS and always allowed; 50+ bots get one switch each; country rules use free GeoIP databases and can close only the checkout; IP and network lists, rate limits, honeypots and the cart flood fix; a one-second challenge instead of a captcha; learning mode, dashboard, log, IP tester and Cloudflare export. No core changes, no OCMOD.