CDEK delivery service integration
CDEK is one of Russia's largest logistics operators covering over 5400 cities with its own pickup point and postamat network. API v2 lets you integrate delivery calculation, pickup point selection on map, order creation, and package tracking directly into your website.
API Authorization
CDEK uses OAuth 2.0 with client credentials grant. Token lives 3600 seconds, then you need a new one:
class CdekAuthService
{
private const TOKEN_URL = 'https://api.cdek.ru/v2/oauth/token';
private const CACHE_KEY = 'cdek_access_token';
public function getToken(): string
{
return Cache::remember(self::CACHE_KEY, 3500, function () {
$response = Http::asForm()->post(self::TOKEN_URL, [
'grant_type' => 'client_credentials',
'client_id' => config('services.cdek.client_id'),
'client_secret' => config('services.cdek.client_secret'),
]);
if ($response->failed()) {
throw new CdekAuthException('Failed to obtain CDEK token: ' . $response->body());
}
return $response->json('access_token');
});
}
}
For testing use separate credentials:
-
client_id:EMscd6r9JnFiQ3bLoyjJY6eM -
client_secret:PjLZkKBHEiLK3YsjtNrt7ZpUzj - Base URL:
https://api.edu.cdek.ru/v2
Shipping rate calculation
class CdekCalculatorService
{
public function calculateTariffList(
string $fromCityCode,
string $toCityCode,
array $packages,
int $deliveryType = 1 // 1=door-door, 2=door-pickup, 3=pickup-door, 4=pickup-pickup
): array {
$response = Http::withToken($this->auth->getToken())
->post('https://api.cdek.ru/v2/calculator/tarifflist', [
'type' => $deliveryType,
'from_location' => ['code' => (int)$fromCityCode],
'to_location' => ['code' => (int)$toCityCode],
'packages' => $packages,
]);
if ($response->failed()) {
throw new CdekApiException($response->body());
}
return collect($response->json('tariff_codes'))
->filter(fn($t) => empty($t['errors']))
->map(fn($t) => [
'code' => $t['tariff_code'],
'name' => $t['tariff_name'],
'cost' => $t['delivery_sum'],
'min_days' => $t['period_min'],
'max_days' => $t['period_max'],
])
->values()
->toArray();
}
}
Main tariffs: 136 — parcel warehouse-warehouse, 137 — parcel warehouse-door, 138 — parcel door-warehouse, 139 — parcel door-door.
Finding CDEK city code
CDEK uses own city codes different from FIAS or KLADR. Search by name:
public function findCityCode(string $cityName, string $countryCode = 'RU'): ?int
{
$response = Http::withToken($this->auth->getToken())
->get('https://api.cdek.ru/v2/location/cities', [
'country_codes' => [$countryCode],
'city' => $cityName,
'size' => 5,
]);
$cities = $response->json('0');
return $cities['code'] ?? null;
}
Cache cities locally — they change rarely.
List of pickup points
public function getPickupPoints(string $cityCode, float $weightKg): array
{
$response = Http::withToken($this->auth->getToken())
->get('https://api.cdek.ru/v2/deliverypoints', [
'city_code' => $cityCode,
'weight_max' => (int)($weightKg),
'type' => 'PVZ',
'is_handout' => 'true',
]);
return collect($response->json())
->map(fn($p) => [
'code' => $p['code'],
'name' => $p['name'],
'address' => $p['location']['address'],
'lat' => $p['location']['latitude'],
'lng' => $p['location']['longitude'],
'work_time' => $p['work_time'],
'cash_allowed'=> $p['have_cash'],
])
->toArray();
}
Creating order
After purchase confirmation, register order in CDEK:
public function createOrder(Order $order): string
{
$payload = [
'tariff_code' => $order->cdek_tariff_code,
'from_location' => [
'code' => config('services.cdek.warehouse_city_code'),
'address' => config('services.cdek.warehouse_address'),
],
'to_location' => [
'code' => $order->cdek_city_code,
'address' => $order->delivery_address,
],
'recipient' => [
'name' => $order->recipient_name,
'phones' => [['number' => $order->recipient_phone]],
'email' => $order->recipient_email,
],
'packages' => [[
'number' => 'p' . $order->id,
'weight' => (int)($order->total_weight_kg * 1000),
'length' => $order->package_length,
'width' => $order->package_width,
'height' => $order->package_height,
'comment' => 'Order #' . $order->id,
'items' => $order->items->map(fn($item) => [
'name' => $item->product->name,
'ware_key'=> (string)$item->product_id,
'cost' => $item->price,
'amount' => $item->quantity,
'weight' => (int)($item->product->weight_g),
])->toArray(),
]],
];
if ($order->pickup_point_code) {
$payload['delivery_point'] = $order->pickup_point_code;
}
$response = Http::withToken($this->auth->getToken())
->post('https://api.cdek.ru/v2/orders', $payload);
$orderId = $response->json('entity.uuid');
if (!$orderId) {
throw new CdekOrderException('Failed to create CDEK order');
}
return $orderId;
}
Status tracking via webhook
CDEK sends status update notifications:
Http::withToken($token)->post('https://api.cdek.ru/v2/webhooks', [
'url' => 'https://yoursite.ru/api/cdek/webhook',
'type' => 'ORDER_STATUS',
]);
public function handleWebhook(Request $request): Response
{
$data = $request->json()->all();
if ($data['type'] !== 'ORDER_STATUS') {
return response('ok', 200);
}
$order = Order::where('cdek_uuid', $data['attributes']['uuid'])->first();
if ($order) {
$order->update([
'cdek_status' => $data['attributes']['status']['code'],
'cdek_status_at' => now(),
]);
if (in_array($data['attributes']['status']['code'], ['READY_FOR_PICKUP', 'DELIVERED'])) {
dispatch(new NotifyCustomerDeliveryStatus($order));
}
}
return response('ok', 200);
}
Key statuses: CREATED, ACCEPTED_AT_SENDER_WAREHOUSE, READY_FOR_PICKUP, DELIVERED, NOT_DELIVERED.
Printing forms
public function getPrintForm(string $cdekUuid): string
{
$response = Http::withToken($this->auth->getToken())
->post('https://api.cdek.ru/v2/print/orders', [
'orders' => [['order_uuid' => $cdekUuid]],
'copy' => 1,
]);
$taskUuid = $response->json('entity.uuid');
for ($i = 0; $i < 10; $i++) {
sleep(2);
$status = Http::withToken($this->auth->getToken())
->get("https://api.cdek.ru/v2/print/orders/{$taskUuid}")
->json();
if ($status['entity']['status'] === 'READY') {
return $status['entity']['url'];
}
}
throw new \RuntimeException('Print form generation timeout');
}
Timeline
Basic integration (rate calculation + pickup points + order creation) — 5–7 working days. Adding webhook tracking, label printing, status sync — 3–4 more days. Testing in CDEK sandbox before switching to production — mandatory, 1–2 days.







