Logistics Company Website Development on 1C-Bitrix
A logistics company website is not a brochure — it is an operational tool. A shipper arrives with a specific task: calculate the cost from point A to point B, place an order, track a shipment. If the calculator returns nothing, tracking shows no status, and reordering requires filling out 12 fields from scratch — the client moves to a competitor with a functioning portal. Bitrix can handle all of this, but the architecture must account for logistics-specific workflows from day one.
Service Catalog: Info Blocks for Transport Types
A logistics provider doesn't sell one product — it offers a matrix of services: FTL (full truckload), LTL (less than truckload), warehousing, customs brokerage, last-mile delivery. Each type carries its own constraints — weight limits, dimensions, temperature requirements, geography.
Data structure:
- Info block "Services" — sections: Road Freight, Sea Freight, Rail, Air, Warehousing, Customs
-
Highload block "Vehicle Types" — reference: curtainside, refrigerated, 20' container, 40' container, insulated. Fields:
UF_NAME,UF_CAPACITY_KG,UF_VOLUME_M3,UF_PHOTO,UF_DESCRIPTION -
Highload block "Routes" — city-to-city pairs linked to transport types. Fields:
UF_FROM_CITY,UF_TO_CITY,UF_TRANSPORT_TYPE,UF_TRANSIT_DAYS,UF_ACTIVE - Highload block "Cargo Constraints" — max weight, dimensions, prohibited categories per transport type
Each service element contains:
| Property | Type | Purpose |
|---|---|---|
| SERVICE_TYPE | L (list) | FTL / LTL / Warehousing / Customs / LastMile |
| TRANSPORT_TYPES | S:Highload (multiple) | Linked vehicle types |
| ROUTE_DIRECTIONS | S:Highload (multiple) | Available routes |
| MAX_WEIGHT | N | Max cargo weight, kg |
| MAX_VOLUME | N | Max volume, m³ |
| TEMPERATURE_MODE | L | Ambient / Chilled / Frozen |
| INSURANCE_AVAILABLE | L | Yes / No |
| CUSTOMS_INCLUDED | L | Yes / No |
For SEO, the high-value pages follow the pattern "[transport type] + [route]": "Road Freight Moscow to Novosibirsk", "LTL Shipping from China". These pages are generated from the junction of the services info block and the routes Highload block via a custom component. URLs follow the template /services/{service-code}/{from}-{to}/, with human-readable slugs configured through CIBlockElement::GetList filtering on ROUTE_FROM and ROUTE_TO properties.
Freight Calculator: The Primary Conversion Point
The calculator is why 70% of visitors land on a logistics website. Not a "leave your number, we'll call back" form — a real calculation: origin, destination, cargo parameters, price. If the calculator doesn't return a number, the visitor never converts.
Calculator architecture has three layers.
Layer 1 — frontend: multi-step form. Step 1: origin and destination (city autocomplete via AJAX, data from a "Cities" Highload block or an external API like Yandex.Geocoder). Step 2: cargo parameters — weight, volume (L×W×H), number of packages, packaging type, temperature requirements. Step 3: additional options — insurance, customs clearance, door-to-door delivery. Step 4: result — price, transit time, available transport modes.
The form is built as a React component or vanilla JS with step navigation. Each step triggers server-side AJAX validation. City autocomplete hits a dedicated endpoint /api/cities/suggest/?q=Mosc, querying b_hlbd_cities (the Highload block) via DataManager::getList() with a filter ['%UF_NAME' => $query].
Layer 2 — distance calculation. Tariffs depend on distance. Two approaches:
Approach A — precomputed distance matrix in a DistanceMatrix Highload block. Fields: UF_FROM_CITY_ID, UF_TO_CITY_ID, UF_DISTANCE_KM, UF_TRANSIT_HOURS. With 500 cities, that's 250K rows — manageable. Advantage: instant response, no external API dependency. Drawback: needs recalculation when cities are added.
Approach B — real-time calculation via an external API. Google Distance Matrix API or Yandex.Routing API (router.route()). Request: two points, response: distance in km plus travel time. Results are cached in the Highload block — if a city pair already exists and is less than 30 days old, return from cache; otherwise, make the API call and store the result.
// Distance retrieval with caching
class DistanceService
{
public static function getDistance(int $fromCityId, int $toCityId): array
{
$cached = DistanceMatrixTable::getList([
'filter' => [
'UF_FROM_CITY_ID' => $fromCityId,
'UF_TO_CITY_ID' => $toCityId,
'>UF_CACHED_AT' => date('Y-m-d', strtotime('-30 days'))
]
])->fetch();
if ($cached) {
return [
'distance_km' => $cached['UF_DISTANCE_KM'],
'transit_hours' => $cached['UF_TRANSIT_HOURS']
];
}
$result = GoogleDistanceMatrixClient::calculate(
Cities::getCoordinates($fromCityId),
Cities::getCoordinates($toCityId)
);
DistanceMatrixTable::add([
'UF_FROM_CITY_ID' => $fromCityId,
'UF_TO_CITY_ID' => $toCityId,
'UF_DISTANCE_KM' => $result['distance'],
'UF_TRANSIT_HOURS' => $result['duration'],
'UF_CACHED_AT' => new DateTime()
]);
return $result;
}
}
Layer 3 — tariff engine. Tariffs live in a Tariffs Highload block:
| Field | Type | Description |
|---|---|---|
| UF_SERVICE_TYPE | list | FTL / LTL / Express |
| UF_TRANSPORT_TYPE | reference | Vehicle type |
| UF_DISTANCE_FROM | number | Range start, km |
| UF_DISTANCE_TO | number | Range end, km |
| UF_RATE_PER_KM | number | Rate per km |
| UF_MIN_RATE | number | Minimum charge |
| UF_WEIGHT_COEFF | number | Overweight multiplier |
| UF_VOLUME_COEFF | number | Volume multiplier |
Calculation formula for LTL: max(distance_km * rate_per_km, min_rate) * weight_coeff * volume_coeff + insurance + customs_fee. For FTL it is simpler: flat rate per km multiplied by distance, no weight coefficients — the entire truck is booked.
Managers update tariffs through the Highload block admin interface — no developer involvement. This is a hard requirement: tariffs change weekly, and if updating a price requires a deployment, the system is unusable.
Calculator response returns as JSON:
{
"variants": [
{
"transport": "Curtainside 20t",
"service": "FTL",
"price": 1200,
"currency": "USD",
"transit_days": 3,
"distance_km": 1800
},
{
"transport": "LTL Consolidated",
"service": "LTL",
"price": 340,
"currency": "USD",
"transit_days": 7,
"distance_km": 1800
}
]
}
Below the result — a "Place Order" button that carries all calculation parameters into the order form. The user does not re-enter data.
Shipment Tracking
Tracking is the second reason clients return to the site. A tracking number input on the homepage, result — a status chain with timestamps and current location on a map.
The data source is 1C:TMS or 1C:Logistics. Integration via Bitrix REST API: 1C pushes status updates through POST /rest/logistics.shipment.updateStatus with fields tracking_number, status_code, location, timestamp. Bitrix stores statuses in a ShipmentStatuses Highload block.
Frontend — an AJAX request by tracking number. The response contains an array of statuses (accepted, at warehouse, in transit, at customs, delivered) and coordinates for the last known location, rendered on Google Maps or Yandex.Maps.
Real-time updates — polling every 60 seconds, or WebSocket if traffic volume justifies the complexity.
Client Portal: The B2B Dashboard
A personal dashboard for corporate clients is what separates a serious logistics provider from a "brochure site with a calculator." It is not just order history — it is a working tool for a logistics manager.
Authentication and roles. A client company registers as a legal entity. Within the company — multiple users with distinct roles. Implementation through Bitrix user groups (CGroup) and custom fields:
- Company admin — sees all orders, manages users, downloads documents, views financials
- Logistics manager — creates orders, tracks statuses, downloads waybills and CMR notes
- Accountant — document access only: invoices, acts, tax invoices
User-to-company binding — via a custom field UF_COMPANY_ID in b_user. Access control — middleware in init.php that checks the user's group and UF_COMPANY_ID on every request to /personal/.
Portal functionality:
Order history — a list of all company shipments, filterable by date, status, route. Data from the Orders Highload block: UF_ORDER_NUMBER, UF_COMPANY_ID, UF_FROM_CITY, UF_TO_CITY, UF_STATUS, UF_CARGO_DESCRIPTION, UF_WEIGHT, UF_VOLUME, UF_PRICE, UF_CREATED_AT. Pagination via bitrix:system.pagenavigation, filtering via AJAX.
Document management — each order carries a set of documents: waybill, CMR, invoice, packing list, insurance certificate. Files are stored as multiple file-type properties linked to the order. Downloads go through a custom controller that verifies the document belongs to the requesting user's company before serving the file. No direct links to /upload/ — authorized access only.
// Document access control
class DocumentController extends Controller
{
public function download(int $orderId, int $fileId): Response
{
$user = $GLOBALS['USER'];
$order = OrdersTable::getById($orderId)->fetch();
if ($order['UF_COMPANY_ID'] !== $user->getUfCompanyId()) {
throw new AccessDeniedException();
}
$file = CFile::GetFileArray($fileId);
return new BinaryFileResponse($file['SRC']);
}
}
Recurring shipment templates. A regular client ships the same cargo on the same routes repeatedly. The logistics manager saves an order as a template — next time, they select the template, adjust the date, confirm. Templates are a separate ShipmentTemplates Highload block mirroring the order structure plus UF_TEMPLATE_NAME and UF_COMPANY_ID.
Financial section — mutual settlements balance, issued invoices, payment history. Data is synchronized from 1C via REST API on a schedule (every 15 minutes) or event-driven.
1C:TMS Integration
Order synchronization between the website and 1C:TMS (or 1C:Fleet Management) is bidirectional:
- Website to 1C: a new order from the site is sent to 1C via REST API. The 1C endpoint accepts JSON with order parameters and creates a "Transport Request" document
-
1C to website: a status change in 1C triggers a webhook to Bitrix. The handler updates
UF_STATUSin theOrdersHighload block and sends an email/SMS notification to the client
Exchange format — JSON over HTTP REST. XML exchange via CommerceML is overkill for logistics — that format is designed for commerce, not freight.
Coverage Map and Fleet
Interactive map — Google Maps or Yandex.Maps with a custom layer. Warehouse and hub markers from the Warehouses Highload block (fields: UF_NAME, UF_ADDRESS, UF_COORDINATES, UF_TYPE, UF_PHOTO). Route lines between hubs — google.maps.Polyline (or ymaps.Polyline) with data from the routes Highload block. Clicking a hub opens an info window with address, working hours, and available services.
Fleet section — an info block with vehicle types. Vehicle card: photo, payload capacity, cargo volume, type (curtainside, refrigerated, container carrier). Rendered via bitrix:news.list with a custom template — a card grid with characteristic icons.
Partner API
REST API for integration with partner systems: freight forwarders, marketplaces, client ERP systems. Endpoints:
-
POST /api/v1/orders/create— order creation -
GET /api/v1/orders/{id}/status— order status -
GET /api/v1/tracking/{number}— shipment tracking -
POST /api/v1/calculate— cost calculation
Authentication — API key in the X-Api-Key header. Keys are generated in the admin panel and bound to a partner company. Rate limiting — 100 requests per minute via middleware.
Stages and Timelines
| Scale | Timeline |
|---|---|
| Brochure site with calculator, up to 10 routes | 4-6 weeks |
| Corporate site with portal, tracking, 1C integration | 10-16 weeks |
| Platform with partner API, B2B portal, full automation | 16-24 weeks |
Timelines exclude 1C-side exchange configuration — that is a separate workstream handled by a 1C developer, running in parallel.







