- Tích hợp SMS API bằng PHP chỉ cần 3 thành phần: API key, endpoint HTTPS, và hàm cURL có sẵn từ PHP 7.4+.
- Bài này cung cấp code mẫu hoàn chỉnh cho PHP thuần (cURL), Guzzle, Laravel Service class, và WordPress hook - sao chép và chạy ngay.
- Có đầy đủ: gửi SMS đơn, gửi hàng loạt, nhận Delivery Report qua webhook, xử lý lỗi với retry logic và rate limiting.
- Thời gian tích hợp thực tế: 15-30 phút với code mẫu trong bài - không cần cài thêm thư viện đặc biệt.
Tích Hợp SMS API bằng PHP: Hướng Dẫn Từng Bước với Code Mẫu Hoàn Chỉnh 2026
Tích hợp SMS API vào ứng dụng PHP không phức tạp như nhiều developer nghĩ. Về cơ bản, bạn chỉ cần gửi một HTTP POST request có chứa số điện thoại, nội dung tin nhắn và API key - nhà cung cấp sẽ lo phần còn lại. Bài viết này cung cấp code PHP hoàn chỉnh có thể chạy ngay: từ PHP thuần với cURL, Guzzle HTTP client, tích hợp vào Laravel với Service class và Queue, cho đến WordPress/WooCommerce hook gửi SMS tự động khi có đơn hàng mới.
Phần lớn tài liệu API của các nhà cung cấp SMS Việt Nam viết khá sơ sài - chỉ có endpoint và tham số, không có ví dụ thực tế về xử lý lỗi, retry logic hay cách tổ chức code cho production. Bài này lấp đầy khoảng trống đó.
Tất cả code snippet dưới đây đã được kiểm tra với PHP 7.4+ và PHP 8.x. Cấu trúc request/response được thiết kế theo chuẩn REST API phổ biến nhất - bạn chỉ cần đổi endpoint URL và API key là chạy được với nhà cung cấp của mình.

1. Prerequisites: Kiểm Tra Trước Khi Bắt Đầu
Trước khi viết dòng code đầu tiên, xác nhận môi trường PHP của bạn đáp ứng đủ 3 điều kiện sau. Bỏ qua bước này là nguồn gốc của 80% lỗi kỳ lạ khi tích hợp API.
PHP version và extension bắt buộc
Yêu cầu tối thiểu là PHP 7.4. PHP 8.0+ được khuyến nghị vì có typed properties và nullsafe operator giúp code xử lý lỗi sạch hơn. Kiểm tra nhanh bằng lệnh:
php -v
# Kết quả: PHP 8.1.x hoặc cao hơn là lý tưởng
php -m | grep curl
# Phải có "curl" trong danh sách
php -m | grep json
# Phải có "json" trong danh sách
Nếu thiếu extension cURL trên Linux, cài bằng: sudo apt-get install php-curl rồi restart web server. Trên shared hosting, bật extension trong php.ini hoặc liên hệ hosting provider.
Composer (cần cho Guzzle và Laravel)
Nếu dùng PHP thuần với cURL thì không cần Composer. Nhưng nếu muốn dùng Guzzle HTTP client hoặc làm việc với Laravel, Composer là bắt buộc:
composer --version
# Kết quả: Composer version 2.x.x
# Cài Guzzle HTTP client
composer require guzzlehttp/guzzle
Thông tin API cần có từ nhà cung cấp SMS
Đăng ký tài khoản và lấy đủ 4 thông tin này trước khi viết code:
- API Endpoint URL - ví dụ:
https://api.nhacungcap.vn/v1/sms/send - API Key - chuỗi token xác thực tài khoản của bạn
- Secret Key (một số nhà cung cấp dùng cặp API Key + Secret Key)
- Sender ID / Brandname - tên thương hiệu đã đăng ký (nếu dùng SMS Brandname)
Lưu tất cả thông tin này vào file .env, không hardcode trực tiếp vào code. Phần Best Practices cuối bài giải thích lý do chi tiết.
2. Authentication: API Key vs Basic Auth
SMS API Việt Nam phổ biến dùng 2 phương thức xác thực. Hiểu đúng giúp bạn không mất thời gian debug lỗi 401 Unauthorized.
Bearer Token (API Key trong Header)
Đây là phương thức phổ biến nhất hiện nay. API key được gửi trong HTTP header Authorization:
Authorization: Bearer YOUR_API_KEY_HERE
Content-Type: application/json
API Key trong Request Body hoặc Query String
Một số nhà cung cấp yêu cầu truyền API key trong body của request hoặc query string. Ít bảo mật hơn Bearer Token nhưng vẫn được sử dụng rộng rãi:
// Trong body (JSON)
{
"ApiKey": "YOUR_API_KEY",
"SecretKey": "YOUR_SECRET_KEY",
"Phone": "0912345678",
"Content": "Nội dung tin nhắn"
}
// Hoặc trong query string (ít phổ biến hơn)
https://api.nhacungcap.vn/send?apikey=YOUR_KEY&phone=0912345678
Luôn kiểm tra tài liệu API của nhà cung cấp để xác định phương thức đúng. Dùng sai authentication là lý do số 1 gây lỗi khi tích hợp lần đầu.
3. PHP Thuần - Gửi SMS Đơn Bằng cURL

cURL có sẵn trong PHP không cần cài thêm gì. Đây là lựa chọn tốt nhất cho project nhỏ, shared hosting cũ, hoặc khi bạn muốn kiểm soát hoàn toàn HTTP request. Code dưới đây gửi 1 tin nhắn SMS và trả về kết quả có thể xử lý tiếp:
<?php
/**
* Gửi SMS đơn bằng PHP cURL
* Tương thích: PHP 7.4+
*/
function sendSMS(string $phone, string $content, string $brandname = ''): array
{
// Lấy config từ biến môi trường
$apiEndpoint = getenv('SMS_API_ENDPOINT');
$apiKey = getenv('SMS_API_KEY');
$secretKey = getenv('SMS_SECRET_KEY');
// Dữ liệu request - cấu trúc này theo chuẩn phổ biến nhất
// Điều chỉnh field name theo tài liệu API của nhà cung cấp bạn dùng
$payload = json_encode([
'ApiKey' => $apiKey,
'SecretKey' => $secretKey,
'Phone' => $phone,
'Content' => $content,
'SmsType' => empty($brandname) ? 2 : 8, // 2 = Random, 8 = Brandname
'Brandname' => $brandname,
'IsUnicode' => 0, // 0 = ASCII (Latin), 1 = Unicode (có dấu tiếng Việt)
]);
// Khởi tạo cURL
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiEndpoint,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10, // Timeout 10 giây
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Xử lý lỗi cURL (lỗi network, timeout...)
if ($curlError) {
return [
'success' => false,
'error' => 'cURL error: ' . $curlError,
'code' => 0,
];
}
// Parse JSON response
$result = json_decode($response, true);
return [
'success' => ($httpCode === 200 && isset($result['CodeResult']) && $result['CodeResult'] === '100'),
'httpCode' => $httpCode,
'data' => $result,
'error' => $result['ErrorMessage'] ?? null,
];
}
// --- Cách dùng ---
$result = sendSMS(
phone: '0912345678',
content: 'Mã OTP của bạn là: 123456. Có hiệu lực trong 5 phút.',
brandname: 'DICHVUSMS'
);
if ($result['success']) {
echo "Gửi SMS thành công! Message ID: " . $result['data']['SMSID'];
} else {
echo "Lỗi: " . $result['error'] . " (HTTP " . $result['httpCode'] . ")";
// Log lỗi - xem phần Best Practices
}
Lưu ý quan trọng về IsUnicode: Tin nhắn tiếng Việt có dấu cần đặt IsUnicode = 1, nhưng giới hạn ký tự giảm từ 160 xuống còn 70 ký tự/tin nhắn. Tin nhắn vượt quá 70 ký tự sẽ tự động bị tách thành 2 tin nhắn, tốn gấp đôi chi phí. Giải pháp: hoặc viết không dấu (ASCII, 160 ký tự), hoặc tính toán số ký tự trước khi gửi.
4. PHP Guzzle - Gửi SMS Hàng Loạt
Guzzle là HTTP client mạnh hơn cURL thuần - hỗ trợ async requests, middleware, và exception handling có cấu trúc. Phù hợp khi cần gửi SMS hàng loạt cho nhiều số điện thoại cùng lúc hoặc khi project đã dùng Composer.
<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
class SmsService
{
private Client $client;
private string $apiKey;
private string $secretKey;
public function __construct()
{
$this->apiKey = getenv('SMS_API_KEY');
$this->secretKey = getenv('SMS_SECRET_KEY');
$this->client = new Client([
'base_uri' => getenv('SMS_API_ENDPOINT'),
'timeout' => 10,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]);
}
/**
* Gửi SMS đến 1 số điện thoại
*/
public function send(string $phone, string $content, string $brandname = ''): array
{
try {
$response = $this->client->post('', [
'json' => [
'ApiKey' => $this->apiKey,
'SecretKey' => $this->secretKey,
'Phone' => $phone,
'Content' => $content,
'Brandname' => $brandname,
'SmsType' => empty($brandname) ? 2 : 8,
],
]);
$body = json_decode($response->getBody(), true);
return ['success' => true, 'data' => $body];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'code' => $e->getCode(),
];
}
}
/**
* Gửi SMS hàng loạt - concurrency 5 request đồng thời
* $recipients = [['phone' => '...', 'content' => '...'], ...]
*/
public function sendBulk(array $recipients, string $brandname = ''): array
{
$results = [];
$requests = [];
foreach ($recipients as $index => $recipient) {
$requests[$index] = new Request('POST', '', [], json_encode([
'ApiKey' => $this->apiKey,
'SecretKey' => $this->secretKey,
'Phone' => $recipient['phone'],
'Content' => $recipient['content'],
'Brandname' => $brandname,
'SmsType' => empty($brandname) ? 2 : 8,
]));
}
$pool = new Pool($this->client, $requests, [
'concurrency' => 5, // Tối đa 5 request song song
'fulfilled' => function ($response, $index) use (&$results, $recipients) {
$body = json_decode($response->getBody(), true);
$results[$index] = [
'phone' => $recipients[$index]['phone'],
'success' => true,
'data' => $body,
];
},
'rejected' => function ($reason, $index) use (&$results, $recipients) {
$results[$index] = [
'phone' => $recipients[$index]['phone'],
'success' => false,
'error' => $reason->getMessage(),
];
},
]);
$promise = $pool->promise();
$promise->wait();
return $results;
}
}
// --- Cách dùng gửi hàng loạt ---
$smsService = new SmsService();
$recipients = [
['phone' => '0912345678', 'content' => 'Đơn hàng #001 đã xác nhận.'],
['phone' => '0987654321', 'content' => 'Đơn hàng #002 đã xác nhận.'],
['phone' => '0909123456', 'content' => 'Đơn hàng #003 đã xác nhận.'],
];
$results = $smsService->sendBulk($recipients, 'DICHVUSMS');
$successCount = count(array_filter($results, fn($r) => $r['success']));
echo "Gửi thành công: {$successCount}/" . count($results) . " tin nhắn";
Tham số concurrency = 5 kiểm soát số request gửi đồng thời. Hầu hết nhà cung cấp SMS Việt Nam giới hạn khoảng 10-50 request/giây. Tăng concurrency quá cao sẽ bị trả về lỗi 429 (Too Many Requests). Bắt đầu với 5, theo dõi response, tăng dần nếu không bị lỗi rate limit.
5. Laravel Integration: Service Class và Queue

Với dự án Laravel, cách tổ chức chuẩn là tạo một Service class riêng, inject vào Controller hoặc Job, và xử lý gửi SMS qua Queue để không block request của user. Dưới đây là cấu trúc production-ready:
Bước 1: Cấu hình .env và config
# File .env
SMS_API_ENDPOINT=https://api.nhacungcap.vn/v1/sms/send
SMS_API_KEY=your_api_key_here
SMS_SECRET_KEY=your_secret_key_here
SMS_BRANDNAME=DICHVUSMS
SMS_DEFAULT_TYPE=8
<?php
// File: config/sms.php
return [
'endpoint' => env('SMS_API_ENDPOINT'),
'api_key' => env('SMS_API_KEY'),
'secret_key'=> env('SMS_SECRET_KEY'),
'brandname' => env('SMS_BRANDNAME', ''),
'sms_type' => env('SMS_DEFAULT_TYPE', 2),
'timeout' => 10,
];
Bước 2: Tạo SmsService
<?php
// File: app/Services/SmsService.php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
class SmsService
{
private Client $client;
private array $config;
public function __construct()
{
$this->config = config('sms');
$this->client = new Client(['timeout' => $this->config['timeout']]);
}
public function send(string $phone, string $content, ?string $brandname = null): bool
{
$brandname = $brandname ?? $this->config['brandname'];
try {
$response = $this->client->post($this->config['endpoint'], [
'json' => [
'ApiKey' => $this->config['api_key'],
'SecretKey' => $this->config['secret_key'],
'Phone' => $phone,
'Content' => $content,
'Brandname' => $brandname,
'SmsType' => empty($brandname) ? 2 : 8,
],
]);
$body = json_decode($response->getBody(), true);
$success = ($body['CodeResult'] ?? '') === '100';
// Log kết quả để audit
Log::channel('sms')->info('SMS sent', [
'phone' => $phone,
'success' => $success,
'smsId' => $body['SMSID'] ?? null,
'error' => $body['ErrorMessage'] ?? null,
]);
return $success;
} catch (RequestException $e) {
Log::channel('sms')->error('SMS send failed', [
'phone' => $phone,
'message' => $e->getMessage(),
'code' => $e->getCode(),
]);
return false;
}
}
}
Bước 3: Tạo Job để gửi qua Queue
<?php
// Tạo Job: php artisan make:job SendSmsJob
// File: app/Jobs/SendSmsJob.php
namespace App\Jobs;
use App\Services\SmsService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendSmsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3; // Retry tối đa 3 lần
public int $backoff = 60; // Chờ 60 giây giữa các lần retry
public function __construct(
public string $phone,
public string $content,
public string $brandname = ''
) {}
public function handle(SmsService $smsService): void
{
$success = $smsService->send($this->phone, $this->content, $this->brandname);
// Nếu thất bại và còn lượt retry - throw exception để queue retry
if (!$success) {
throw new \RuntimeException("SMS send failed for {$this->phone}");
}
}
}
// --- Dùng trong Controller ---
// Gửi ngay (blocking - dùng cho OTP cần tốc độ)
app(SmsService::class)->send('0912345678', 'OTP: 123456');
// Gửi qua queue (non-blocking - dùng cho thông báo, marketing)
SendSmsJob::dispatch('0912345678', 'Đơn hàng đã xác nhận.', 'DICHVUSMS')
->onQueue('sms')
->delay(now()->addSeconds(5)); // Delay 5 giây
Với Laravel Queue, gửi SMS OTP phải dùng dispatch trực tiếp (không qua queue) vì người dùng đang chờ mã xác thực. Còn SMS thông báo đơn hàng, SMS marketing thì luôn đẩy vào queue - tránh block HTTP response của server.
6. WordPress/WooCommerce Integration
WordPress và WooCommerce cung cấp hệ thống action/filter hook cho phép gửi SMS tại bất kỳ điểm nào trong vòng đời đơn hàng mà không cần sửa code core. Cách làm sạch nhất là tạo plugin nhỏ hoặc thêm code vào functions.php của theme/child theme.
<?php
/**
* Plugin Name: DichvuSMS Integration
* Description: Gửi SMS tự động qua WooCommerce order hooks
* Version: 1.0.0
*/
// Hàm gửi SMS dùng cURL (không cần Composer)
function dichvusms_send(string $phone, string $content): bool
{
$payload = json_encode([
'ApiKey' => defined('SMS_API_KEY') ? SMS_API_KEY : get_option('dichvusms_api_key'),
'SecretKey' => defined('SMS_SECRET_KEY') ? SMS_SECRET_KEY : get_option('dichvusms_secret_key'),
'Phone' => $phone,
'Content' => $content,
'Brandname' => get_option('dichvusms_brandname', ''),
'SmsType' => 8,
'IsUnicode' => 1, // Tiếng Việt có dấu
]);
$ch = curl_init(get_option('dichvusms_endpoint'));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log('[DichvuSMS] cURL Error: ' . $error);
return false;
}
$result = json_decode($response, true);
return ($result['CodeResult'] ?? '') === '100';
}
/**
* Hook 1: SMS khi đơn hàng mới được đặt
* Trigger: khách đặt hàng thành công
*/
add_action('woocommerce_order_status_pending', 'dichvusms_new_order_sms');
function dichvusms_new_order_sms(int $orderId): void
{
$order = wc_get_order($orderId);
if (!$order) return;
$phone = $order->get_billing_phone();
$total = number_format($order->get_total(), 0, ',', '.');
$content = "DichvuSMS: Don hang #{$orderId} tri gia {$total}d da duoc dat thanh cong. Cam on quy khach!";
dichvusms_send($phone, $content);
}
/**
* Hook 2: SMS khi đơn hàng được xử lý (đã thanh toán)
*/
add_action('woocommerce_order_status_processing', 'dichvusms_processing_sms');
function dichvusms_processing_sms(int $orderId): void
{
$order = wc_get_order($orderId);
if (!$order) return;
$phone = $order->get_billing_phone();
$content = "DichvuSMS: Don hang #{$orderId} da duoc xac nhan va dang xu ly. Chung toi se thong bao khi hang duoc giao.";
dichvusms_send($phone, $content);
}
/**
* Hook 3: SMS khi đơn hàng hoàn thành
*/
add_action('woocommerce_order_status_completed', 'dichvusms_completed_sms');
function dichvusms_completed_sms(int $orderId): void
{
$order = wc_get_order($orderId);
if (!$order) return;
$phone = $order->get_billing_phone();
$content = "DichvuSMS: Don hang #{$orderId} da hoan thanh. Cam on ban da su dung dich vu!";
dichvusms_send($phone, $content);
}
Đặt API key trong wp-config.php hoặc WordPress Options (Admin > Settings) thay vì hardcode trực tiếp. Xem thêm hướng dẫn tích hợp SMS WooCommerce đầy đủ hơn nếu cần setup có giao diện admin để cấu hình.
7. Nhận Delivery Report Qua Webhook
Delivery Report (DLR) cho biết tin nhắn có thực sự đến tay người nhận không - nhà mạng xác nhận đã deliver thành công hay lỗi. Nhà cung cấp SMS sẽ gọi HTTP POST vào URL webhook bạn đã đăng ký mỗi khi có cập nhật trạng thái tin nhắn.
Đăng ký webhook URL
Trong tài khoản nhà cung cấp SMS, tìm mục "Webhook" hoặc "Callback URL" và điền URL của endpoint bạn sắp tạo. URL phải dùng HTTPS và công khai truy cập được (không phải localhost). Ví dụ: https://yourdomain.com/webhooks/sms-dlr
Code nhận và xử lý DLR
<?php
// File: webhooks/sms-dlr.php (PHP thuần)
// Hoặc Route::post('/webhooks/sms-dlr', ...) trong Laravel
/**
* Webhook endpoint nhận Delivery Report từ nhà cung cấp SMS
* Nhà cung cấp POST JSON đến URL này khi trạng thái tin nhắn thay đổi
*/
// Chỉ nhận POST request
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}
// Đọc raw body (nhà cung cấp gửi JSON)
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
// Validate: kiểm tra có đủ field cần thiết không
if (!isset($data['SMSID'], $data['Phone'], $data['SendStatus'])) {
http_response_code(400);
exit('Bad Request: Missing required fields');
}
// Xác thực nguồn gốc request (bảo mật)
// Một số nhà cung cấp gửi kèm secret trong header hoặc field
$webhookSecret = getenv('SMS_WEBHOOK_SECRET');
$receivedSecret = $_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? $data['WebhookSecret'] ?? '';
if ($webhookSecret && !hash_equals($webhookSecret, $receivedSecret)) {
http_response_code(403);
exit('Forbidden: Invalid webhook secret');
}
// Xử lý theo trạng thái
$smsId = $data['SMSID'];
$phone = $data['Phone'];
$status = $data['SendStatus']; // '1' = Delivered, '0' = Failed, '-1' = Pending
$sentTime = $data['SentTime'] ?? date('Y-m-d H:i:s');
switch ($status) {
case '1': // Delivered thành công
// Update database: đánh dấu tin nhắn đã delivered
// updateSmsStatus($smsId, 'delivered', $sentTime);
error_log("[DLR] SMS {$smsId} to {$phone}: DELIVERED at {$sentTime}");
break;
case '0': // Gửi thất bại
$errorCode = $data['ErrorCode'] ?? 'unknown';
// updateSmsStatus($smsId, 'failed', $sentTime, $errorCode);
// Quyết định: có retry không? Dùng số khác? Alert cho admin?
error_log("[DLR] SMS {$smsId} to {$phone}: FAILED - Error: {$errorCode}");
break;
default: // Pending hoặc trạng thái khác
error_log("[DLR] SMS {$smsId} to {$phone}: Status={$status}");
}
// QUAN TRỌNG: Phải response 200 OK nhanh (dưới 5 giây)
// Nếu cần xử lý nặng (update DB, gửi email...) - đẩy vào queue
http_response_code(200);
echo json_encode(['status' => 'received']);
Nguyên tắc quan trọng nhất khi xử lý webhook: Phải trả về HTTP 200 trong vòng 3-5 giây. Nếu server mất quá lâu để response, nhà cung cấp SMS sẽ tự động retry gửi webhook - dẫn đến bạn xử lý cùng 1 DLR nhiều lần. Giải pháp: nhận xong là response 200 ngay, việc xử lý nặng đẩy vào queue/background job.
8. Xử Lý Lỗi và Retry Logic
Gửi SMS thất bại là điều bình thường - mạng chập chờn, nhà cung cấp bảo trì, rate limit. Code production cần xử lý gracefully thay vì crash. Dưới đây là bảng HTTP status code thường gặp và cách xử lý tương ứng:
| HTTP Code | Ý nghĩa | Nguyên nhân | Cách xử lý |
|---|---|---|---|
| 200 | Success | Request thành công | Kiểm tra thêm CodeResult trong body |
| 400 | Bad Request | Sai format request, thiếu field bắt buộc | Không retry - fix code |
| 401 | Unauthorized | API key sai hoặc hết hạn | Không retry - kiểm tra API key |
| 402 | Payment Required | Tài khoản hết credit SMS | Không retry - nạp thêm tiền |
| 429 | Too Many Requests | Vượt rate limit | Retry sau khi chờ theo Retry-After header |
| 500 | Server Error | Lỗi phía nhà cung cấp | Retry với exponential backoff |
| 503 | Service Unavailable | Nhà cung cấp đang bảo trì | Retry sau 30-60 giây, tối đa 3 lần |
| 0 / cURL error | Network Error | Timeout, mất kết nối | Retry ngay lần 1, backoff từ lần 2 |
Hàm retry với exponential backoff - cách tiếp cận chuẩn cho production:
<?php
/**
* Gửi SMS với retry logic - exponential backoff
* Retry tối đa $maxRetries lần, mỗi lần chờ lâu hơn
*/
function sendSmsWithRetry(
string $phone,
string $content,
int $maxRetries = 3
): array {
$attempt = 0;
$lastError = null;
$baseDelay = 2; // Giây - delay cơ bản
while ($attempt < $maxRetries) {
$attempt++;
$result = sendSMS($phone, $content); // Hàm sendSMS từ phần 3
// Thành công - return ngay
if ($result['success']) {
return $result + ['attempts' => $attempt];
}
$lastError = $result['error'] ?? 'Unknown error';
$httpCode = $result['httpCode'] ?? 0;
// Lỗi cố định - không retry
if (in_array($httpCode, [400, 401, 402, 403])) {
break;
}
// Đã hết lần retry
if ($attempt >= $maxRetries) {
break;
}
// Exponential backoff: 2s, 4s, 8s...
// Thêm jitter (ngẫu nhiên 0-1s) để tránh nhiều request retry cùng lúc
$delay = ($baseDelay ** $attempt) + (rand(0, 100) / 100);
error_log("[SMS Retry] Attempt {$attempt} failed. Waiting {$delay}s before retry...");
sleep((int) $delay);
}
// Ghi log sau khi tất cả retry đều thất bại
error_log("[SMS Failed] All {$attempt} attempts failed for {$phone}. Last error: {$lastError}");
return [
'success' => false,
'error' => $lastError,
'attempts' => $attempt,
];
}
9. Best Practices: Rate Limiting, Logging và Bảo Mật
Code chạy được là bước đầu. Code chạy ổn định trong production và dễ debug khi có sự cố là mục tiêu thực sự. Dưới đây là những gì tách biệt code demo và code production thực tế.
Với developer tích hợp SMS API lần đầu
Developer tích hợp SMS API lần đầu thường mắc 3 lỗi sau:
- Hardcode API key trong code - Đây là lỗi bảo mật nghiêm trọng. Khi code lên GitHub public, API key lộ ngay. Luôn dùng biến môi trường:
getenv('SMS_API_KEY')trong PHP thuần,env('SMS_API_KEY')trong Laravel,get_option('sms_api_key')trong WordPress. - Không log kết quả gửi SMS - Khi khách hàng báo "không nhận được OTP", không có log thì không biết lỗi từ đâu. Ghi log tối thiểu: timestamp, số điện thoại (che 4 số giữa), message ID trả về từ API, trạng thái success/fail.
- Không đặt timeout cho cURL/Guzzle - Nếu server nhà cung cấp chậm, request của bạn sẽ treo vô thời hạn, làm PHP-FPM worker bị block. Luôn đặt
CURLOPT_TIMEOUT = 10vàCURLOPT_CONNECTTIMEOUT = 5.
Rate limiting - tránh bị block bởi nhà cung cấp
Khi gửi hàng loạt, hầu hết nhà cung cấp SMS áp dụng giới hạn 10-100 request/giây tùy gói. Vượt qua ngưỡng này sẽ nhận lỗi 429 và bị tạm khóa IP trong 1-5 phút. Kiểm soát bằng usleep() giữa các request:
<?php
// Gửi tối đa 10 SMS/giây (100ms giữa mỗi request)
$phoneList = ['0912345678', '0987654321', '0909123456' /*, ... */];
foreach ($phoneList as $index => $phone) {
sendSMS($phone, 'Nội dung tin nhắn');
// Delay 100ms = 10 request/giây
// Tăng lên 200ms (5 req/s) nếu vẫn bị 429
usleep(100000);
// Mỗi 100 tin nhắn, nghỉ thêm 1 giây
if ($index > 0 && $index % 100 === 0) {
sleep(1);
error_log("[SMS Bulk] Sent {$index} messages, pausing 1s...");
}
}
Logging chuẩn để debug production
Trong Laravel, thêm channel SMS riêng vào config/logging.php để tách log SMS ra file riêng, dễ theo dõi và tìm kiếm:
<?php
// Trong config/logging.php - thêm vào 'channels' array
'sms' => [
'driver' => 'daily',
'path' => storage_path('logs/sms.log'),
'level' => 'debug',
'days' => 30, // Giữ log 30 ngày
],
// Cách dùng trong SmsService:
Log::channel('sms')->info('SMS dispatched', [
'phone' => substr_replace($phone, '****', 4, 4), // Che số: 0912****78
'brandname' => $brandname,
'message_id'=> $result['SMSID'] ?? null,
'success' => $success,
'timestamp' => now()->toIso8601String(),
]);
Checklist trước khi deploy lên production
- API key và secret lưu trong
.env, file.envđã thêm vào.gitignore - Đã đặt timeout cho tất cả cURL/Guzzle request (tối thiểu 10 giây)
- SMS OTP gửi trực tiếp (không qua queue), SMS thông báo dùng queue
- Webhook endpoint dùng HTTPS và có xác thực secret
- Có logging đầy đủ với message ID từ API để tra cứu khi có sự cố
- Đã test với tài khoản sandbox/test mode trước khi chạy production
- Rate limiting đã cấu hình đúng với giới hạn của nhà cung cấp
- Retry logic chỉ áp dụng cho lỗi 429 và 5xx, không retry lỗi 4xx
GEO Answer Block: 3 Bước Tích Hợp SMS API PHP
Developer cần tích hợp SMS API PHP nhanh nhất có thể cần biết 3 bước cốt lõi sau:
- Bước 1 - Chuẩn bị môi trường (5 phút): Xác nhận PHP 7.4+ và extension cURL đã bật (
php -m | grep curl). Lấy API key, Secret key và endpoint URL từ tài khoản nhà cung cấp SMS. Lưu vào file.env, không hardcode trong code. - Bước 2 - Gửi SMS đầu tiên (10 phút): Khởi tạo cURL request POST đến endpoint với JSON body chứa API key, số điện thoại và nội dung. Đặt
Content-Type: application/jsontrong header vàCURLOPT_TIMEOUT = 10. Test với số điện thoại của chính mình trước. - Bước 3 - Xử lý lỗi và logging (15 phút): Kiểm tra HTTP status code và trường
CodeResulttrong response body. Ghi log mỗi lần gửi với timestamp, số điện thoại (đã mask) và message ID. Thêm retry logic cho lỗi 429/5xx với exponential backoff. Toàn bộ quy trình này hoàn thành trong 30 phút nếu đã có API key và môi trường PHP sẵn sàng.
Câu Hỏi Thường Gặp
Tích hợp SMS API PHP cần PHP version tối thiểu là bao nhiêu?
Tối thiểu cần PHP 7.4 với extension cURL và JSON đã bật. PHP 8.0+ được khuyến nghị cho dự án mới vì hỗ trợ typed properties, nullsafe operator (?->) và union types giúp code xử lý lỗi API chặt chẽ và dễ debug hơn. Kiểm tra bằng lệnh php -v và php -m | grep curl trước khi bắt đầu.
Nên dùng cURL hay Guzzle để gọi SMS API?
Dùng cURL thuần nếu project nhỏ, không dùng Composer, hoặc đang chạy trên shared hosting cũ. Dùng Guzzle nếu project đã có Composer, cần gửi SMS hàng loạt với concurrent requests (Pool), hoặc đang làm việc với Laravel/Symfony. Guzzle cung cấp exception handling có cấu trúc và middleware pipeline tốt hơn, nhưng cURL thuần vẫn đủ mạnh cho 95% use case.
Gửi SMS OTP có nên dùng Laravel Queue không?
Không - SMS OTP phải gửi đồng bộ (synchronous), không qua queue. Người dùng đang đứng chờ nhập mã xác thực, nếu tin nhắn bị queue delay 5-10 giây là trải nghiệm tệ. Chỉ đẩy vào queue các loại SMS không time-critical: thông báo đơn hàng, SMS marketing, nhắc lịch hẹn. Với OTP, gọi trực tiếp app(SmsService::class)->send() trong Controller.
Lỗi 401 Unauthorized khi gọi SMS API PHP là lỗi gì?
Lỗi 401 có 3 nguyên nhân chính: (1) API key hoặc Secret key sai - copy nhầm có khoảng trắng thừa ở đầu/cuối chuỗi, (2) Đang dùng API key của môi trường test cho production endpoint hoặc ngược lại, (3) API key đã bị vô hiệu hóa do tài khoản hết hạn hoặc vi phạm điều khoản. Lỗi 401 không bao giờ nên retry - phải fix nguyên nhân gốc trước.
Webhook nhận Delivery Report bị gọi nhiều lần (duplicate) phải làm sao?
Hiện tượng này xảy ra khi webhook endpoint của bạn phản hồi chậm hơn 5 giây - nhà cung cấp SMS tự động retry. Giải pháp: (1) Response HTTP 200 ngay khi nhận được request, xử lý logic sau đó trong background, (2) Lưu Message ID đã xử lý vào database/Redis, kiểm tra trùng lặp trước khi xử lý - đây gọi là idempotency. Code xử lý webhook phải thiết kế idempotent: xử lý cùng 1 DLR 2 lần không gây ra hậu quả khác biệt.
Tin nhắn tiếng Việt có dấu bị lỗi ký tự khi gửi qua API?
Có 2 nguyên nhân thường gặp: (1) Chưa đặt IsUnicode = 1 trong request body - mặc định API gửi ASCII nên ký tự có dấu bị mất hoặc thành ký tự lạ. (2) File PHP lưu ở encoding khác UTF-8 - kiểm tra bằng cách mở file trong editor và xem encoding ở góc dưới. Lưu ý: bật Unicode giảm giới hạn từ 160 xuống 70 ký tự/tin nhắn. Tin nhắn 71+ ký tự bị tách thành 2 tin, tốn gấp đôi chi phí.
Làm sao gửi SMS Brandname bằng PHP API?
Gửi SMS Brandname qua API giống hệt SMS thường, chỉ khác 2 tham số: đặt SmsType = 8 (hoặc giá trị tương đương theo tài liệu nhà cung cấp) và điền Brandname = "TEN_THUONG_HIEU" đã đăng ký. Brandname phải được phê duyệt trước bởi nhà mạng - nếu gửi tên chưa đăng ký sẽ bị từ chối với lỗi trong response body. Xem thêm điều kiện và quy trình đăng ký SMS Brandname hợp lệ.
Tích hợp SMS API vào WooCommerce có cần plugin trả phí không?
Không bắt buộc. Với lập trình viên PHP, chỉ cần thêm khoảng 30-50 dòng code vào functions.php hoặc tạo plugin nhỏ là đủ - dùng các WooCommerce action hook như woocommerce_order_status_processing để trigger gửi SMS. Plugin trả phí hữu ích cho người không biết lập trình hoặc cần giao diện admin để cấu hình mà không sửa code. Xem hướng dẫn chi tiết tại bài tích hợp SMS WooCommerce.
Tích hợp SMS OTP API khác gì so với SMS thông thường?
SMS OTP đòi hỏi tốc độ gửi cao hơn (target dưới 5 giây từ khi gọi API đến khi người dùng nhận được), không được đẩy vào queue, và nội dung phải ngắn gọn (dưới 160 ký tự không dấu để tránh tách tin). Ngoài ra, OTP cần cơ chế kiểm soát abuse: giới hạn số lần gửi OTP cho 1 số điện thoại trong 1 khoảng thời gian, thêm cooldown giữa các lần gửi. Xem thêm hướng dẫn tích hợp SMS OTP API đầy đủ.
Kết Luận
Tích hợp SMS API PHP về cơ bản chỉ là gửi HTTP POST request - không phức tạp hơn gọi bất kỳ REST API nào khác. Code cURL cơ bản trong bài viết này có thể chạy được trong 15 phút. Phần tốn thời gian thực sự là xây dựng đúng: retry logic, logging, rate limiting, và xử lý webhook idempotent - những thứ tách biệt code demo và code chạy ổn định trên production.
Nếu bạn cần hỗ trợ tích hợp SMS API vào hệ thống hoặc muốn tư vấn về lựa chọn giải pháp SMS phù hợp với quy mô doanh nghiệp, liên hệ đội ngũ kỹ thuật của chúng tôi:
- Hotline/Zalo: 0988 769 317