- Tested on OpenCart 4.1.0.4 (PHP 8.3 store; unit tests on PHP 8.1 and 8.3) against a stand-in of the Etsy API, not a live Etsy shop.
by Kavio Labs · for OpenCart 4.1.x
Integrates OpenCart 4 with your Etsy shop through the Etsy Open API v3 and your own Etsy app (OAuth 2.0 with PKCE): draft listings with SKU, price, quantity, images, tags and properties, stock and price sync, paid Etsy orders imported once into OpenCart orders, tracking sent back to Etsy, optional signed webhooks, a job queue with retries and a full log. OpenCart 4, no OCMOD. The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
Integrates OpenCart 4 with your Etsy shop through the Etsy Open API v3 and your own Etsy app (OAuth 2.0 with PKCE): draft listings with SKU, price, quantity, images, tags and properties, stock and price sync, paid Etsy orders imported once into OpenCart orders, tracking sent back to Etsy, optional signed webhooks, a job queue with retries and a full log. OpenCart 4, no OCMOD. The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
Etsy Connector 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.
| 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 |
The Documentation tab covers the settings and every screen.
OpenCart 4.1.0.4. Tested on OpenCart 4.1.0.4. The Compatibility tab lists every version range.
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.
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
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
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.
11 more answers about setup and daily use in the Documentation tab.
The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
Integrates OpenCart 4.1 with your Etsy shop through the Etsy Open API v3 and your own Etsy app. Your products become Etsy listings with SKU, price, quantity, images, tags and properties, stock and prices follow your store, paid Etsy orders arrive as real OpenCart orders (each exactly once) and the tracking number goes back to Etsy when you ship. OpenCart 4, events only, no OCMOD, no core file changed.

cron.php), or the web address shown on the Jobs page.https://<your shop>/extension/kv_etsy/return.php.kv_etsy.ocmod.zip, click the green install button next to it.order.paid, order.canceled and order.shipped, and paste the signing secret (whsec_...) into the Connection page.OpenCart 4.1 updates an extension by uninstall + install. The links between Etsy receipts and OpenCart orders, between products and Etsy listings, the connection, the rules and the settings are kept on uninstall, so an update never imports an order or creates a listing twice. Nothing is pushed to Etsy right after a reinstall until a product changes or the hourly check comes round.
| Setting | Meaning |
|---|---|
| Status | Off: no new work is started; the admin pages stay open. |
| Status of imported orders | Order status of a new Etsy order (Processing takes the stock, like a normal order). |
| Sync interval | Minutes between two order imports / stock pushes / shipment reports (default 15). |
| Debug log | Also stores every HTTP call (masked). Switch off when done. |
| Log retention | Days the log is kept (default 90). |
| Licence key | Your Kavio Labs licence. Without a key the extension works 14 days; the admin is never blocked. |
Keystring, shared secret and the optional webhook signing secret (stored encrypted), the callback URL to register in your Etsy app, the shop authorization and the listing defaults: shipping profile (required), return policy, processing profile, who made it, when it was made, supply, "Publish new listings" and "Import orders from".

| Rule | Meaning |
|---|---|
| Markup / fixed amount | Added to the OpenCart price (special price included). Category and product markups replace the general one. |
| Rounding | No rounding, whole numbers, endings .99 or .90. |
| VAT added to net prices | 0 by default. OpenCart prices are stored without tax; EU / UK shops whose Etsy prices include VAT enter the rate, and products with a tax class get it before the markups. |
| Exchange rate | Etsy shop currency per 1 unit of your default currency. 1 = the OpenCart currency table. If the Etsy currency is missing there and no rate is set, listings, prices and stock are not sent (shown on the Rules page). |
| Stock buffer / sold out at or below / highest quantity | What Etsy sees of your stock. |
| Create listings automatically | Off: only stock and price of products already on Etsy are synced. |
| Customer | Guest orders, or attach to a customer with the same e-mail (Etsy gives the buyer e-mail only to approved apps). |
| Wait for a tracking number | On: an order is reported shipped only once its history has a tracking number. |
| Status mapping | OpenCart status for Shipped, Cancelled on Etsy and Fully refunded on Etsy. |

Search Etsy's seller taxonomy, map it to a store category with a markup. On the Properties page fill each Etsy property from a product attribute, the manufacturer, the model, the SKU or a fixed value; required properties must have one.

Add a product by its ID (the SKU on Etsy is the product SKU, else the model) or all products of a category. With "Already on Etsy" the product is linked to the listing that has this SKU instead of creating a new one. At the next sync each product becomes an Etsy draft with its price, quantity, images, tags and properties; switch on "Publish new listings" or publish the drafts on etsy.com. The Listings page shows the listing ID (a link to Etsy), the last quantity and price pushed and Etsy's answer when something was refused (Retry).
Stock and price changes go out with the next sync. Etsy decides how a listing at quantity 0 is shown.
Paid Etsy receipts become OpenCart orders with the order date of the receipt. The order comment holds the receipt number, the payment method, the buyer note and the gift message; the order page has an Etsy panel with the receipt status and the tracking found.

Tracking: 9400111899223197428490 Carrier: USPS. Etsy gets the shipment with the carrier matched to Etsy's carrier list ("other" when none matches). It is sent once.
The Log page lists every step with filters and a CSV export; the Jobs page shows the queue with Run the queue now, Retry and Remove, and the cron command and web address.
The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
OpenCart 4.1 port of the CS-Cart add-on kv_etsy 1.0.1 (same API client, mapper, driver and hub logic, with every fix of its release audit research/quality/AUDIT-2026-11-07-etsy.md). Extension code kv_etsy (package kv_etsy.ocmod.zip, licence id kv_oc_etsy), PHP namespace KavioLabs\KvEtsy in extension/kv_etsy/system/library/ (autoloaded by bootstrap.php). Built on the Kavio Marketplace Hub (Hub\*, channel independent) with one channel driver: Client\Api (HTTP), Drivers\Etsy\Mapper (pure translation, unit tested), Drivers\Etsy\Driver (Hub\DriverInterface). OpenCart glue: Store (products, orders, statuses, tracking), compat.php (db_* helpers over the OpenCart DB object), Keep (data kept across uninstall + install), oc4/return.php (OAuth redirect, see below).
| Source | URL | Local copy |
|---|---|---|
| Open API v3 specification 3.0.0 (the reference) | https://www.etsy.com/openapi/generated/oas/3.0.0.json | scratchpad/etsy/docs-raw/oas.json |
| Authentication (API key, OAuth 2.0, PKCE, scopes, refresh) | https://developers.etsy.com/documentation/essentials/authentication | docs-raw/essentials_authentication.txt |
| Request standards (hosts, headers, UTF-8) | https://developers.etsy.com/documentation/essentials/requests | docs-raw/essentials_requests.txt |
| Rate limits | https://developers.etsy.com/documentation/essentials/rate-limits | docs-raw/essentials_rate-limits.txt |
| Webhooks (events, payloads, signatures) | https://developers.etsy.com/documentation/essentials/webhooks | docs-raw/essentials_webhooks.txt |
| App types, commercial access criteria, required trademark sentence | https://developers.etsy.com/documentation/ | docs-raw/documentation_index.txt |
node scratchpad/etsy/show.mjs op <operationId> prints the parameters and body fields of an operation from the spec copy.
https://api.etsy.com (requests guide: api.etsy.com and openapi.etsy.com "are equivalent"; the spec lists openapi.etsy.com).x-api-key: <keystring>:<shared secret> (authentication guide, "API Key"; spec securitySchemes.api_key).Authorization: Bearer <user id>.<token> (requests guide). Bodies are UTF-8; form bodies are sent as application/x-www-form-urlencoded; charset=utf-8 (requests guide, "Encoding").module_kv_etsy_base_url (e.g. http://127.0.0.1:11931) replaces the host for every call, including the token endpoint (the authorization page https://www.etsy.com/oauth/connect is opened by the seller's browser and is never called by the store). tests/e2e.js refuses to run unless it points at 127.0.0.1 / localhost.| Step | Call | Fields |
|---|---|---|
| 1 | browser GET https://www.etsy.com/oauth/connect | response_type=code, client_id (keystring), redirect_uri (https, exact match), scope (space separated), state, code_challenge (base64url SHA-256 of the verifier), code_challenge_method=S256 |
| 2 | Etsy redirects to <shop>/extension/kv_etsy/return.php, which continues same-site to index.php?route=extension/kv_etsy/channel/etsy.authorize | code, state (or error) |
| 3 | POST /v3/public/oauth/token (form) | grant_type=authorization_code, client_id, redirect_uri, code, code_verifier -> access_token (1 h, <user id>. prefix), refresh_token (90 days), expires_in, scope |
| refresh | POST /v3/public/oauth/token (form) | grant_type=refresh_token, client_id, refresh_token -> new access AND new refresh token |
Redirect (OpenCart 4.1): the session cookie OCSESSID is shared by admin and catalog and sent with SameSite=Strict. A browser coming from etsy.com does not send it, so OpenCart would start a new session and replace the cookie: the admin would be signed out. The redirect URI is therefore oc4/return.php (generated by kv return-file oc_etsy --platform opencart --oauth): it boots nothing, sets no cookie and passes on only state (exact pattern), code ([A-Za-z0-9._~-]{1,512}), error ([a-z_]{1,64}) and language with a same-site navigation (meta refresh + location.replace) to the fixed route. The route itself also removes any queued Set-Cookie when a request arrives without the session cookie (webhooks, a direct hit). The redirect URI used in the link is stored (etsy:redirect:0) and repeated exactly in the code exchange. A registered URI without a query string also settles open point 3 below.
Implementation: Accounts::authState() = <account>.<time>.<nonce>.<HMAC> (1 hour, single use via Accounts::consumeState()); Accounts::verifier() = 43 base64url characters derived from the state with the store secret (RFC 7636: 43-128 of [A-Za-z0-9._~-]), so no verifier is stored. Api::challenge() is checked against RFC 7636 Appendix B and the pair in Etsy's guide (unit tests). The refresh runs under GET_LOCK (Idempotency::lock('token-...')) and re-reads the stored set after taking the lock, because every refresh rotates the refresh token. A refresh answered 400/401/403 (e.g. invalid_grant) PAUSES the account (auth.paused, connection check "Paused"): Accounts::active() is false, cron schedules nothing and Hub::driver() returns null until the seller connects again.
Scopes requested (Api::SCOPES), with the endpoints that need them (spec security of each operation):
| Scope | Used by |
|---|---|
listings_r | getListingsByShop, getListingInventory |
listings_w | createDraftListing, updateListing, updateListingInventory, uploadListingImage, updateListingProperty |
shops_r | getMe, getShopShippingProfiles, getShopReadinessStateDefinitions |
transactions_r | getShopReceipts, getShopReceipt |
transactions_w | createReceiptShipment |
listings_d, email_r, address_r are not requested (no delete; buyer e-mail access is a separate approval for commercial apps).
| Operation (spec operationId) | Method + path | Fields used |
|---|---|---|
| ping | GET /v3/application/openapi-ping | API key only (connection test before authorization) |
| getMe | GET /v3/application/users/me | user_id, shop_id |
| getShop | GET /v3/application/shops/{shop_id} | shop_name, currency_code, url |
| getSellerTaxonomyNodes | GET /v3/application/seller-taxonomy/nodes | id, name, parent_id, children |
| getPropertiesByTaxonomyId | GET /v3/application/seller-taxonomy/nodes/{taxonomy_id}/properties | property_id, display_name, is_required, supports_attributes, is_multivalued, possible_values[].value_id/name |
| getShopShippingProfiles | GET /v3/application/shops/{shop_id}/shipping-profiles | shipping_profile_id, title, origin_country_iso, is_deleted |
| getShopReturnPolicies | GET /v3/application/shops/{shop_id}/policies/return | return_policy_id, accepts_returns, return_deadline |
| getShopReadinessStateDefinitions | GET /v3/application/shops/{shop_id}/readiness-state-definitions | readiness_state_id, readiness_state, processing_days_display_label |
| getShippingCarriers | GET /v3/application/shipping-carriers?origin_country_iso= | name |
| createDraftListing | POST /v3/application/shops/{shop_id}/listings (form) | required quantity, title, description, price, who_made, when_made, taxonomy_id; shipping_profile_id (required for physical), return_policy_id, readiness_state_id, tags, is_supply, type=physical, item_weight + item_weight_unit |
| updateListing | PATCH /v3/application/shops/{shop_id}/listings/{listing_id} (form) | state=active (publish; "requires that the listing have an image") |
| getListingsByShop | GET /v3/application/shops/{shop_id}/listings | state, limit (max 100), offset, includes=Inventory; reads listing_id, title, state, created_timestamp, inventory.products[].sku |
| getListingInventory | GET /v3/application/listings/{listing_id}/inventory | products[].sku/offerings/property_values, *_on_property |
| updateListingInventory | PUT /v3/application/listings/{listing_id}/inventory (JSON) | products[]{sku, property_values[]{property_id, value_ids, values, scale_id}, offerings[]{price, quantity, is_enabled, readiness_state_id}}, price_on_property, quantity_on_property, sku_on_property, readiness_state_on_property |
| uploadListingImage | POST /v3/application/shops/{shop_id}/listings/{listing_id}/images (multipart) | image (file), rank, alt_text (max 500) |
| updateListingProperty | PUT /v3/application/shops/{shop_id}/listings/{listing_id}/properties/{property_id} (form) | value_ids, values |
| getShopReceipts | GET /v3/application/shops/{shop_id}/receipts | min_last_modified, limit (max 100), offset, sort_on=updated, sort_order=asc; reads ShopReceipt incl. transactions |
| getShopReceipt | GET /v3/application/shops/{shop_id}/receipts/{receipt_id} | reads shipments[].tracking_code before a tracking push; webhook follow-up |
| createReceiptShipment | POST /v3/application/shops/{shop_id}/receipts/{receipt_id}/tracking (JSON) | tracking_code, carrier_name, send_bcc=false |
Receipt -> order (Mapper::order): receipt_id, status, is_paid, is_shipped, name (split at the last space, mb-safe), first_line, second_line, city, state, zip, country_iso, buyer_email (null unless Etsy approved access: a placeholder address is used), payment_method, message_from_buyer, is_gift, gift_message, grandtotal, total_shipping_cost, total_tax_cost, total_vat_cost, discount_amt, created_timestamp, updated_timestamp, shipments[], transactions[] (transaction_id, sku, title, quantity, price, listing_id, variations[].formatted_name/formatted_value). Money = amount / divisor (spec, Money schema).
Status (ShopReceipt.status enum paid, completed, open, payment processing, canceled, fully refunded, partially refunded): paid -> created (shipped when is_shipped), completed -> shipped, canceled -> cancelled, fully refunded -> returned, partially refunded -> created / shipped. open and payment processing are pending: never imported, never mapped to cancelled.
QPS and QPD per API key; 429 with retry-after (seconds); headers x-limit-per-second, x-remaining-this-second, x-limit-per-day, x-remaining-today (rate-limits guide). Api::call() waits once for a retry-after of 1-3 s; anything longer is thrown as retryable and the job queue backs off (5 min, 15 min, 1 h, 3 h, 12 h). A run that stops on a 429 keeps what it created (see "Never twice").
Events order.paid, order.canceled, order.shipped, order.delivered, listing.*, inventory.*; payload event_type, resource_url, shop_id (+ listing_id, product_id). Subscriptions are made in Etsy's webhook portal (no API). Signature (Webhook::verify): base64(HMAC-SHA256(base64_decode(secret without "whsec_"), webhook-id + "." + webhook-timestamp + "." + raw body)) compared with every entry of webhook-signature; |now - webhook-timestamp| <= 300. The event is stored as a queue job keyed by webhook-id; order events are read back with getShopReceipt only when resource_url matches https://(api|openapi).etsy.com/v3/application/shops/<our shop>/receipts/<id> (the URL itself is never fetched).
Hub\ProductSync::created). A retry looks the SKU up in the cache, then in the shop index (getListingsByShop includes=Inventory, every state), then, when a create was pending, in the drafts created since the marker with the same title, and links what it finds.?:kv_etsy_order_map row claimed with INSERT IGNORE (unique company + receipt) before the order is placed, per-receipt GET_LOCK, placing_at marker; Store::createOrder() writes the marker [kv_etsy:<account>:<receipt>] into the order comment in the same INSERT that creates the order (and removes a half-written order before it rethrows), so an interrupted attempt adopts its order instead of placing a second one. A receipt cancelled at Etsy before it could be imported is parked (retry_at = 2147483647) and never re-read.updated_timestamp of the last receipt READ (option order_cursor), not the local time of the last success; failed receipts are re-read by id (getShopReceipt) once their wait is over or when "Retry" is clicked.Cuts use mb_substr / mb_strlen with UTF-8 (Mapper::cut). Title: allowed character class and "%, :, & and + once each" from the spec; tags: allowed characters from the spec; styles 2 x 45 (spec, not used); alt_text 500 (spec).
Etsy timestamps are Unix seconds. The order date is the receipt's created_timestamp; dates shown in notes use date() (the shop time zone OpenCart sets from System > Settings > Local). "Import orders from" is a calendar day in the shop time zone (Accounts::importFrom, DateTime with the PHP default zone). The Etsy Shop object has no time zone field (spec Shop schema), so the OpenCart shop zone is the one used.
Mapper (TITLE_MAX, TAGS_MAX, TAG_LEN). If Etsy rejects a longer value it answers 400 and the product keeps the error.tags, value_ids, values): the spec has no encoding object, so the OpenAPI 3 default (style form, explode true: tags=a&tags=b) is used; the tag description says "comma-separated list". Not verified against the live API.<shop>/extension/kv_etsy/return.php has none).v1, prefix are taken from the Standard Webhooks convention and accepted both ways.minimum=0 per offering; whether Etsy accepts 0 on the only enabled offering (sold out) or wants the offering disabled is not documented. The extension sends 0 and shows Etsy's answer.tests/mock-server/server.js, which follows the documented contract above.| Topic | OpenCart 4.1 |
|---|---|
| Order creation | Store::createOrder() writes order, order_product (name + Etsy variation), order_total (sub-total, Etsy discount, shipping, tax collected by Etsy, total) and the status history directly: no cart, no checkout events, no e-mails; stock is taken / returned by Store::setStatus() like OpenCart does for processing / complete statuses. |
| Currency | OpenCart currency.value = units per 1 default unit, so default amount = Etsy amount / value (Orders::rateFor), Etsy price = OpenCart price x value (Listings::fx). |
| Net prices | OpenCart prices are stored without tax. vat_add (Rules, default 0) adds a VAT rate to products whose tax class has a percentage rate (Listings::withVat). |
| Status follow | Only an order still in the status mapped for the previous Etsy state follows Etsy; cancellation and full refund always follow. Our own "shipped" report is not echoed back onto the order. |
| Tracking | From the order history: "Tracking: X" / "Tracking number X" (must contain a digit) and "Carrier: name" (Store::parseTracking); due when the order reached the shipped status (or a complete status); claim N -> R per row, stale claims released after 15 minutes. |
| Product data | Name / description / tags (product_description.tag, else meta_keyword), SKU (product_code sku, else model), weight converted to kg (sent as item_weight_unit=kg), images read from DIR_IMAGE (realpath confined). |
| Lost create answer | A draft adopted through the pending marker also gets its SKU / quantity, image, property and publish steps (fix of the port; the CS-Cart add-on links it without the steps). |
| Updates | Uninstall keeps every table (Install::drop($purge = false)), settings are restored by Keep; an install over kept data marks listed rows clean so nothing is pushed before a change. |
| Licence | kv_oc_etsy; when the extension is off or unlicensed the queue starts no new work, the admin pages always open. |
| What you see | What to do |
|---|---|
| "Invalid API key" on Save and test | Keystring and shared secret must both be those of your Etsy app (Etsy sends them as keystring:shared secret). |
| Etsy shows "redirect_uri mismatch" | Register the callback URL exactly as shown on the Connection page (https, same host and spelling). |
| "The link is not valid any more" | The authorization link is valid for one hour and once: click Connect Etsy shop again. |
| "Etsy rejected the authorization. Syncing is paused" | The app was removed or access expired (90 days unused). Click Connect again. |
| Listing error "shipping_profile_id is required" | Load the profiles and choose a shipping profile on the Connection page. |
| "No channel category" on a product | Map one of the product's categories on the Categories page. |
| "Required attribute ... has no value" | Fill that Etsy property on the Properties page. |
| Prices are not sent, Rules page names a currency | Add the Etsy shop currency in System > Localisation > Currencies or set an exchange rate. |
| Import failed "No product mapping for SKU" | Add the product with that SKU on the Listings page, then Retry on the Orders page. |
| Order not reported shipped | The order must reach the mapped shipped status (or Complete); with "Wait for a tracking number" the history needs "Tracking: ...". |
| "Refused by Etsy" on a shipment | Read the error (e.g. Etsy's partner rule, 403), then Send shipment again if it can work now. |
| Nothing happens at all | Check the extension status and licence on the settings page and that the cron runs (Jobs page). When the extension is off or unlicensed, no job runs (also no shipment report); queued jobs run once it is on again. |
| Shop in maintenance mode | Webhooks and the cron web address get the maintenance page; the OpenCart command-line cron keeps working, and the import catches up from its cursor afterwards. |
No. The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc. It integrates with the Etsy Open API v3 through your own Etsy app.
No. Events and cron only.
No, OpenCart 4.1 only.
Yes, a free Etsy app for your own shop gives the keystring and shared secret.
extension/kv_etsy/return.php?OpenCart 4.1 keeps its session cookie strictly same-site. A page that a browser reaches straight from etsy.com would start a new session and sign you out of the admin. This small page sets no cookie and takes you to the extension's own page from inside your shop, so your admin session is kept.
No, they stay drafts unless you switch on "Publish new listings". Etsy charges its listing fee when a listing is published.
No. The receipt is recorded before the order is created, every receipt has its own lock, and an interrupted import adopts the order it already created.
No, Etsy's API has no cancel endpoint. Cancel it on etsy.com; the OpenCart order follows.
Handled on etsy.com. A full refund there moves the OpenCart order to your refunded status.
Etsy gives the buyer e-mail only to apps it approved for it; otherwise orders get a placeholder address ending in @channel.invalid.
Enter your VAT rate under Rules > "VAT added to net prices"; products with a tax class get it before the markups.
Buy with confidence
This product was tested on this build. Builds we did not test are not listed.
Updates and support are included in the price. The product keeps working afterwards; renewing is optional.
How licensing worksIf it is not what you expected, tell us within 30 days and we refund the order.
Refund policyPay by card, Apple Pay or Google Pay on Stripe's secure page. Your card number never reaches our server.
Ask a pre-sales questionSold by Kavio Labs Software Limited, registered in England and Wales No. 17532441. About us
More for OpenCart
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.