PHP

PHP 택배API 연동 가이드

cURL / Guzzle을 사용한 택배 조회 및 Laravel 웹훅
JavaScript Python Java PHP C#
이 가이드에서 다루는 내용
• cURL을 사용한 API 호출
• Guzzle HTTP 클라이언트 예제
• Laravel 웹훅 컨트롤러
• 에러 처리 패턴

1. 설치 (Guzzle 사용 시)

composer require guzzlehttp/guzzle

cURL만 사용한다면 추가 설치 없이 PHP 기본 기능으로 가능합니다.

2. 환경 변수 설정

# .env 파일
DELIVERY_PUBLIC_KEY=pk_live_your_public_key
DELIVERY_SECRET_KEY=sk_live_your_secret_key
WEBHOOK_SECRET=your_webhook_secret
<?php
// PHP 기본
$publicKey = getenv('DELIVERY_PUBLIC_KEY');
$secretKey = getenv('DELIVERY_SECRET_KEY');

// Laravel
$publicKey = env('DELIVERY_PUBLIC_KEY');
$secretKey = env('DELIVERY_SECRET_KEY');
보안 주의
Secret Key를 코드에 직접 하드코딩하지 마세요. 환경 변수를 사용하세요.

3. 배송 조회 (cURL)

기본 예제

<?php

define('PUBLIC_KEY', getenv('DELIVERY_PUBLIC_KEY'));
define('SECRET_KEY', getenv('DELIVERY_SECRET_KEY'));
define('BASE_URL', 'https://api.deliveryapi.co.kr/v1');

function trackDelivery(string $courierCode, string $trackingNumber): array
{
    $url = BASE_URL . '/tracking/trace';
    $payload = json_encode([
        'items' => [[
            'courierCode' => $courierCode,
            'trackingNumber' => $trackingNumber
        ]]
    ]);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . PUBLIC_KEY . ':' . SECRET_KEY,
            'Content-Type: application/json'
        ]
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new Exception('API Error: ' . $response);
    }

    $data = json_decode($response, true);
    return $data['data']['results'][0];  // 첫 번째 결과 반환
}

// 사용 예시
try {
    $result = trackDelivery('cj', '1234567890');

    if ($result['success']) {
        $data = $result['data'];
        echo '배송 상태: ' . $data['deliveryStatus'] . PHP_EOL;
        echo '택배사: ' . $data['courierName'] . PHP_EOL;
    } else {
        echo '조회 실패: ' . $result['message'] . PHP_EOL;
    }

    // 배송 이력 출력
    foreach ($result['data']['progresses'] as $progress) {
        echo "  ${$progress['dateTime']} - ${$progress['description']}" . PHP_EOL;
    }
} catch (Exception $e) {
    echo '에러: ' . $e->getMessage();
}

4. Guzzle 사용 예제

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class DeliveryApiClient
{
    private Client $client;
    private string $publicKey;
    private string $secretKey;
    private string $baseUrl = 'https://api.deliveryapi.co.kr/v1';

    public function __construct(string $publicKey, string $secretKey)
    {
        $this->publicKey = $publicKey;
        $this->secretKey = $secretKey;
        $this->client = new Client([
            'base_uri' => $this->baseUrl,
            'headers' => [
                'Authorization' => 'Bearer ' . $publicKey . ':' . $secretKey,
                'Content-Type' => 'application/json'
            ]
        ]);
    }

    public function track(string $courierCode, string $trackingNumber): array
    {
        try {
            $response = $this->client->post('/tracking/trace', [
                'json' => [
                    'items' => [[
                        'courierCode' => $courierCode,
                        'trackingNumber' => $trackingNumber
                    ]]
                ]
            ]);

            $data = json_decode($response->getBody(), true);
            return $data['data']['results'][0];  // 첫 번째 결과 반환
        } catch (RequestException $e) {
            $errorBody = $e->getResponse()?->getBody()->getContents();
            throw new Exception('API Error: ' . $errorBody);
        }
    }

    public function getCouriers(): array
    {
        $response = $this->client->get('/tracking/couriers');
        return json_decode($response->getBody(), true);
    }
}

// 사용 예시
$api = new DeliveryApiClient(
    getenv('DELIVERY_PUBLIC_KEY'),
    getenv('DELIVERY_SECRET_KEY')
);
$result = $api->track('cj', '1234567890');
print_r($result['data']);

5. 택배사 목록 조회

<?php

function getCouriers(): array
{
    $url = BASE_URL . '/tracking/couriers';

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . PUBLIC_KEY . ':' . SECRET_KEY
        ]
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true);
}

// 결과 예시
$couriers = getCouriers();
foreach ($couriers['data'] as $courier) {
    echo "${$courier['code']}: ${$courier['name']}" . PHP_EOL;
}
// lotte: 롯데택배
// cj: CJ대한통운
// hanjin: 한진택배

6. Laravel 웹훅 컨트롤러

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;

class DeliveryWebhookController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        // 서명 검증
        $signature = $request->header('X-Webhook-Signature');
        $timestamp = $request->header('X-Webhook-Timestamp');
        $payload = $request->getContent();

        if (!$this->verifySignature($payload, $signature, $timestamp)) {
            return response()->json(['error' => 'Invalid signature'], 401);
        }

        $data = $request->json();
        $event = $data->get('event');
        $items = $data->get('items');

        foreach ($items as $item) {
            switch ($event) {
                case 'tracking.polled':
                    $this->handleStatusChanged($item);
                    break;

                case 'tracking.completed':
                    $this->handleDelivered($item);
                    break;
            }
        }

        return response()->json(['received' => true]);
    }

    private function verifySignature(string $payload, ?string $signature, ?string $timestamp): bool
    {
        if (!$signature || !$timestamp) {
            return false;
        }

        // timestamp 유효성 검증 (5분 허용)
        if (abs(time() - (int) $timestamp) > 300) {
            return false;
        }

        // 서명 대상: timestamp.payload
        $secret = config('services.delivery.webhook_secret');
        $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);

        // 헤더값: sha256={hex} → prefix 제거 후 비교
        $received = str_replace('sha256=', '', $signature);

        return hash_equals($expected, $received);
    }

    private function handleStatusChanged(array $data): void
    {
        Log::info('배송 상태 변경', [
            'trackingNumber' => $data['trackingNumber'],
            'status' => $data['currentStatus']
        ]);

        // 비즈니스 로직 처리
        // 예: 주문 상태 업데이트, 고객 알림 등
    }

    private function handleDelivered(array $data): void
    {
        Log::info('배송 완료', [
            'trackingNumber' => $data['trackingNumber']
        ]);

        // 배송 완료 처리
    }
}

// routes/api.php
Route::post('/webhook/delivery', [DeliveryWebhookController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

7. 에러 처리 및 재시도

<?php

function trackWithRetry(string $courier, string $trackingNumber, int $maxRetries = 3): array
{
    $lastException = null;

    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        try {
            return trackDelivery($courier, $trackingNumber);
        } catch (Exception $e) {
            $lastException = $e;

            // 재시도 가능 여부 확인
            if (isRetryable($e) && $attempt < $maxRetries) {
                $delay = pow(2, $attempt); // 지수 백오프 (초)
                echo "재시도 ${$attempt}/${$maxRetries}, ${$delay}초 후..." . PHP_EOL;
                sleep($delay);
                continue;
            }

            throw $e;
        }
    }

    throw $lastException;
}

function isRetryable(Exception $e): bool
{
    $message = $e->getMessage();
    // 서버 에러 또는 네트워크 에러
    return strpos($message, '5') !== false
        || strpos($message, 'timeout') !== false
        || strpos($message, 'connect') !== false;
}

// 에러 코드별 처리
function handleApiError(string $errorResponse): string
{
    $error = json_decode($errorResponse, true);
    $code = $error['code'] ?? '';

    $messages = [
        'INVALID_TRACKING_NUMBER' => '잘못된 송장번호입니다',
        'COURIER_NOT_SUPPORTED' => '지원하지 않는 택배사입니다',
        'RATE_LIMIT_EXCEEDED' => '요청 한도를 초과했습니다',
        'UNAUTHORIZED' => 'API 키가 유효하지 않습니다',
    ];

    return $messages[$code] ?? '일시적인 오류가 발생했습니다';
}

8. WordPress 플러그인 예제

<?php
/**
 * Plugin Name: Delivery Tracker
 * Description: 택배 조회 기능
 */

class DeliveryTracker
{
    private string $apiKey;

    public function __construct()
    {
        $this->apiKey = get_option('delivery_api_key');
        add_shortcode('delivery_track', [$this, 'renderTrackingForm']);
        add_action('wp_ajax_track_delivery', [$this, 'ajaxTrack']);
        add_action('wp_ajax_nopriv_track_delivery', [$this, 'ajaxTrack']);
    }

    public function renderTrackingForm(): string
    {
        ob_start();
        ?>
        <form id="delivery-form">
            <select name="courier">
                <option value="lotte">롯데택배</option>
                <option value="cj">CJ대한통운</option>
                <option value="hanjin">한진택배</option>
            </select>
            <input type="text" name="tracking_number" placeholder="송장번호">
            <button type="submit">조회</button>
        </form>
        <div id="delivery-result"></div>
        <?php
        return ob_get_clean();
    }

    public function ajaxTrack(): void
    {
        $courier = sanitize_text_field($_POST['courier']);
        $trackingNumber = sanitize_text_field($_POST['tracking_number']);

        try {
            $result = $this->track($courier, $trackingNumber);
            wp_send_json_success($result['data']);
        } catch (Exception $e) {
            wp_send_json_error($e->getMessage());
        }
    }

    private function track(string $courier, string $trackingNumber): array
    {
        // cURL 호출 로직 (위 예제 참고)
    }
}

다른 언어 가이드

지금 바로 시작하세요!

무료 플랜으로 API를 바로 테스트해보세요

무료로 시작하기 →