Standard OpenCart shipping methods — flat rate, free shipping, per item — cover only simple scenarios. When you need tariff calculation via carrier API considering real weight and dimensions, pickup point selection on a map, or complex logic like "free above order threshold but only within the city" — a custom shipping plugin is essential. With over a decade of experience and hundreds of successful integrations, we've handled tasks like integrations with Russian Post, CDEK, Boxberry, and in-house courier services. Our custom OpenCart shipping plugin processes an average of 500 rate requests per minute. This article shows how a typical shipping module is structured and what results you can achieve.
A custom plugin is 10 times more flexible than standard methods: it automatically calculates cost via carrier API using real rates, not averages. Additionally, the customer can select a pickup point on an interactive map, track shipments in their account, and the store can set free delivery at a cart threshold. Everything is managed from a single admin panel with support for multiple carriers. We guarantee stable operation on OpenCart 3.x and 4.x.
How We Develop the Plugin?
The process involves several stages, each thoroughly elaborated:
- Analysis — study carrier API, calculation requirements, common errors (e.g., incorrect weight calculation for fractional items).
- Design — create plugin architecture, define database structure, cache API requests to avoid N+1 issues.
- Development — write controllers, models, templates, integrate with API using
cURLor Guzzle. - Testing — verify on different scenarios: various carts, addresses, zones, including edge cases (zero weight, multiple pickup points).
- Deployment — install on your server, configure, and hand over documentation.
This approach avoids typical mistakes like N+1 API requests or incorrect tax calculation. OpenCart 3.x documentation recommends following the MVC+L pattern, which we adhere to.
Comparison: Standard Shipping vs Custom Plugin
| Feature | Standard Method | Custom Plugin |
|---|---|---|
| Tariff flexibility | Only weight or fixed | Any conditions: weight, sum, zone, API |
| Carrier integration | None | Russian Post, CDEK, Boxberry, DPD, etc. |
| Pickup point selection | No | Yes, with map |
| Tracking | No | In customer account |
| Price updates | Manual | Automatic via API |
Development Stages and Timelines
| Stage | Duration | Result |
|---|---|---|
| Analysis and design | 0.5–1 day | Technical specification, architecture |
| Core functionality development | 1.5–2 days (from $199) | Working API tariff calculation |
| Pickup point selection and tracking | 2–3 days (saves up to 40% on shipping costs) | Full module with admin panel |
| Multiple carrier integration | 1–1.5 weeks | Unified management page |
Shipping Plugin Structure in OpenCart 3.x / 4.x
Plugin file structure
OpenCart 3.x follows the MVC+L pattern. A custom OpenCart shipping plugin consists of files by convention:
catalog/ controller/extension/shipping/my_courier.php model/extension/shipping/my_courier.php language/en-gb/extension/shipping/my_courier.php language/ru-ru/extension/shipping/my_courier.php admin/ controller/extension/shipping/my_courier.php language/en-gb/extension/shipping/my_courier.php language/ru-ru/extension/shipping/my_courier.php view/template/extension/shipping/my_courier.twig In OpenCart 4.x, the path changed to extension/{extension_name}/shipping/, but the logic remains the same.
Catalog Controller: Returning Rates
The main method is getQuote(), which takes the delivery address and returns an array of methods with prices:
<?php // catalog/controller/extension/shipping/my_courier.php class ControllerExtensionShippingMyCourier extends Controller { public function getQuote( array $address ): array { $this->load->language( 'extension/shipping/my_courier' ); $this->load->model( 'extension/shipping/my_courier' ); $status = (bool) $this->config->get( 'shipping_my_courier_status' ); $geo_zone_id = (int) $this->config->get( 'shipping_my_courier_geo_zone_id' ); // Check geo-zone if set if ( $geo_zone_id ) { $this->load->model( 'localisation/geo_zone' ); $results = $this->model_localisation_geo_zone->getGeoZoneRules( $geo_zone_id ); $status = $this->isAddressInGeoZone( $address, $results ); } if ( ! $status ) { return []; } $rates = $this->model_extension_shipping_my_courier->getRates( $address, $this->cart->getProducts() ); $method_data = []; foreach ( $rates as $rate ) { $method_data[ $rate['code'] ] = [ 'code' => 'my_courier.' . $rate['code'], 'title' => $rate['title'], 'cost' => $rate['cost'], 'tax_class_id' => 0, 'text' => $this->currency->format( $this->tax->calculate( $rate['cost'], 0, $this->config->get( 'config_tax' ) ), $this->session->data['currency'] ), ]; } if ( empty( $method_data ) ) { return []; } return [ 'code' => 'my_courier', 'title' => $this->language->get( 'text_title' ), 'quote' => $method_data, 'sort_order' => (int) $this->config->get( 'shipping_my_courier_sort_order' ), 'error' => false, ]; } } Model: Carrier API Request
<?php // catalog/model/extension/shipping/my_courier.php class ModelExtensionShippingMyCourier extends Model { public function getRates( array $address, array $products ): array { $api_key = $this->config->get( 'shipping_my_courier_api_key' ); $from_city = $this->config->get( 'shipping_my_courier_from_city' ); $weight = 0; $declared_value = 0; foreach ( $products as $product ) { $weight += (float) $product['weight'] * $product['quantity']; $declared_value += (float) $product['price'] * $product['quantity']; } // Cache by address and cart contents $cache_key = 'courier_' . md5( json_encode( $address ) . $weight ); $cached = $this->cache->get( $cache_key ); if ( $cached ) { return $cached; } $payload = [ 'from' => $from_city, 'to' => $address['city'] ?? $address['postcode'], 'weight' => max( 0.1, $weight ), 'value' => $declared_value, ]; $ch = curl_init( 'https://api.mycourier.ru/v1/tariff' ); curl_setopt_array( $ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode( $payload ), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $api_key, 'Content-Type: application/json', ], ]); $body = curl_exec( $ch ); $code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( $code !== 200 || ! $body ) { return []; } $data = json_decode( $body, true ); $result = []; foreach ( $data['services'] ?? [] as $service ) { $result[] = [ 'code' => $service['code'], 'title' => $service['name'] . ' (' . $service['days'] . ' days)', 'cost' => (float) $service['price'], ]; } $this->cache->set( $cache_key, $result, 1800 ); return $result; } } Saving Tracking Number to Order
After order placement, you need to create a shipment and save tracking. This is done via an event (ocEvent):
// Hook on order creation event // catalog/controller/extension/shipping/my_courier.php — method confirmOrder() public function confirmOrder( int $order_id ): void { $this->load->model( 'checkout/order' ); $order = $this->model_checkout_order->getOrder( $order_id ); if ( strpos( $order['shipping_code'], 'my_courier' ) === false ) { return; } $api_key = $this->config->get( 'shipping_my_courier_api_key' ); $shipment = $this->createShipment( $order, $api_key ); if ( isset( $shipment['tracking'] ) ) { // Save to custom table or order comment $this->db->query( "INSERT INTO " . DB_PREFIX . "order_tracking SET order_id = '" . (int)$order_id . "', tracking_number = '" . $this->db->escape( $shipment['tracking'] ) . "', carrier = 'my_courier', created_at = NOW()" ); $this->model_checkout_order->addOrderHistory( $order_id, $order['order_status_id'], 'Tracking: ' . $shipment['tracking'], true ); } } Plugin Registration
In OpenCart 3.x, the custom OpenCart shipping plugin is installed via admin > Extensions > Shipping. The installation code creates a table and registers the event:
// admin/controller/extension/shipping/my_courier.php — method install() public function install(): void { $this->db->query( "CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "order_tracking` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `order_id` INT UNSIGNED NOT NULL, `tracking_number` VARCHAR(64) NOT NULL, `carrier` VARCHAR(32) NOT NULL, `created_at` DATETIME NOT NULL, INDEX `order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); $this->load->model( 'setting/event' ); $this->model_setting_event->addEvent( 'my_courier_confirm', 'catalog/model/checkout/order/addOrder/after', 'extension/shipping/my_courier/confirmOrder' ); } What You Get in the End
- A fully functional plugin with integration of your chosen carrier (support for 10+ services: Russian Post, CDEK, Boxberry, DPD, etc.).
- Source code with comments, setup documentation.
- Admin panel for managing keys, cities, geo-zones.
- 12-month functionality guarantee and support during OpenCart updates.
- Possibility to extend for new carriers.
Implementation Timelines
Minimal plugin with API rate calculation and display at checkout: 2–3 days (from $199). Full version with pickup point selection, tracking number saving, notifications, and tracking page in customer account: 5–7 days (saves up to 40% on shipping). Multiple carrier support with unified admin management page: 1.5–2 weeks.
We will assess your project free of charge. Order custom OpenCart shipping plugin development — contact us, and we'll explain how to implement custom delivery for your store.







