Интеграция службы доставки СДЭК на сайт
СДЭК — один из крупнейших логистических операторов России с покрытием более 5400 городов, собственной сетью ПВЗ и постаматов. API v2 позволяет подключить расчёт стоимости, выбор ПВЗ на карте, создание заказов на доставку и отслеживание посылок прямо из вашего сайта.
Авторизация в API
СДЭК использует OAuth 2.0 с client credentials grant. Токен живёт 3600 секунд, потом нужен новый:
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');
});
}
}
Для тестирования используются отдельные credentials:
-
client_id:EMscd6r9JnFiQ3bLoyjJY6eM -
client_secret:PjLZkKBHEiLK3YsjtNrt7ZpUzj - базовый URL:
https://api.edu.cdek.ru/v2
Расчёт стоимости доставки
class CdekCalculatorService
{
public function calculateTariffList(
string $fromCityCode,
string $toCityCode,
array $packages,
int $deliveryType = 1 // 1=дверь-дверь, 2=дверь-ПВЗ, 3=ПВЗ-дверь, 4=ПВЗ-ПВЗ
): 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,
// packages: [['weight'=>500, 'length'=>20, 'width'=>15, 'height'=>10]]
]);
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();
}
}
Основные тарифы: 136 — посылка склад-склад, 137 — посылка склад-дверь, 138 — посылка дверь-склад, 139 — посылка дверь-дверь. Для юридических лиц — другой набор кодов.
Получение кода города СДЭК
СДЭК использует собственные коды городов, не совпадающие с ФИАС или КЛАДР. Поиск по названию:
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;
}
Рекомендуется кешировать справочник городов локально — он меняется редко, а запросы к /location/cities суммируются.
Список пунктов выдачи
public function getPickupPoints(
string $cityCode,
float $weightKg,
bool $cashAllowed = false
): array {
$response = Http::withToken($this->auth->getToken())
->get('https://api.cdek.ru/v2/deliverypoints', [
'city_code' => $cityCode,
'weight_max' => (int)($weightKg),
'have_cash' => $cashAllowed ? 'true' : null,
'type' => 'PVZ', // PVZ или POSTAMAT
'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();
}
Создание заказа
После подтверждения покупки — регистрируем заказ в СДЭК:
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->id,
'items' => $order->items->map(fn($item) => [
'name' => $item->product->name,
'ware_key'=> (string)$item->product_id,
'payment' => ['value' => 0], // если уже оплачен
'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: ' . json_encode($response->json('requests.0.errors'))
);
}
return $orderId;
}
Отслеживание статусов через webhook
СДЭК умеет отправлять уведомления о смене статуса заказа на ваш URL:
// Регистрация webhook (делается один раз)
Http::withToken($token)->post('https://api.cdek.ru/v2/webhooks', [
'url' => 'https://yoursite.ru/api/cdek/webhook',
'type' => 'ORDER_STATUS',
]);
// Обработчик webhook
public function handleWebhook(Request $request): Response
{
$data = $request->json()->all();
// Проверка типа события
if ($data['type'] !== 'ORDER_STATUS') {
return response('ok', 200);
}
$cdekOrderUuid = $data['attributes']['uuid'];
$statusCode = $data['attributes']['status']['code'];
$order = Order::where('cdek_uuid', $cdekOrderUuid)->first();
if ($order) {
$order->update([
'cdek_status' => $statusCode,
'cdek_status_at' => now(),
]);
// Уведомить покупателя при ключевых статусах
if (in_array($statusCode, ['READY_FOR_PICKUP', 'DELIVERED'])) {
dispatch(new NotifyCustomerDeliveryStatus($order, $statusCode));
}
}
return response('ok', 200);
}
Ключевые статусы: CREATED (принято), ACCEPTED_AT_SENDER_WAREHOUSE (принято на складе отправителя), READY_FOR_PICKUP (прибыло в ПВЗ), DELIVERED (доставлено), NOT_DELIVERED (не доставлено).
Печать накладных
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');
// Ждём генерации (polling, обычно 2–5 секунд)
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']; // ссылка на PDF
}
}
throw new \RuntimeException('Print form generation timeout');
}
Сроки и объём работ
Базовая интеграция (расчёт стоимости + ПВЗ на карте + создание заказов) — 5–7 рабочих дней. Добавление отслеживания через webhook, печати накладных, синхронизации статусов — ещё 3–4 дня. Тестирование в тестовой среде СДЭК перед переключением на боевую — обязательный этап, занимает 1–2 дня.







