Khách trả tiền trên hệ thống của bạn → gọi một lệnh sang đây → quầy trao SIM → webhook báo về kèm số seri thật. Máy chủ: https://app.simpickup.com/api/partner/v1
Mỗi lời gọi kèm khoá API ở tiêu đề:
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx
Khoá chỉ hiện một lần lúc tạo. Mất thì lấy khoá khác, mất mười giây. Khoá API không gọi được gì khác ngoài /api/partner/v1/*.
Khai IP máy chủ của bạn trong trang quản trị, khoá chỉ dùng được từ đúng những địa chỉ đó. Lộ khoá ra ngoài cũng không ai gọi được từ máy khác.
203.0.113.7 một máy 203.0.113.0/24 cả dải
Gọi từ IP chưa khai thì nhận 403 kèm chính IP vừa gọi, để bạn biết phải khai gì. Đổi máy chủ thì sửa danh sách, không phải lấy khoá mới.
| sk_test_… | sk_live_… | |
|---|---|---|
| Kho SIM | riêng, SIM giả, tự nạp lại được | kho thật của bạn |
| Đơn hàng | riêng, KHÔNG ai ra sân bay | quầy SẼ ra sân bay |
| Đối soát cuối tháng | không tính | có tính |
| Tự đánh dấu đã giao | được (mục 10) | không |
Đây là hai kho dữ liệu khác nhau, không phải một cái cờ. Đơn chạy thử không bao giờ lọt sang danh sách thật, và ngược lại.
Một địa chỉ duy nhất cho cả hai. Đổi từ thử nghiệm sang thật là đổi khoá, không phải đổi địa chỉ — nên không có chỗ nào để sót khi lên thật.
curl -X POST https://app.simpickup.com/api/partner/v1/orders \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"partnerRef": "WEB-2026-00123",
"airportCode": "SGN",
"carrierCode": "vinaphone",
"qty": 2,
"flightNo": "VJ862",
"meetAt": "2026-09-25T08:00:00+07:00",
"guestName": "Kim Min-jun",
"guestEmail": "kim@example.com"
}'
| trường | bắt buộc | ghi chú |
|---|---|---|
| partnerRef | ✅ | Mã đơn bên bạn. Đây là thứ chống tạo trùng — xem mục 4 |
| airportCode | ✅ | SGN, HAN, DAD… lấy từ GET /catalog |
| carrierCode | ✅ | vinaphone, local… lấy từ GET /catalog |
| qty | ✅ | Số SIM, 1–50 |
| flightNo | ✅ | Số hiệu chuyến bay |
| meetAt | ✅ | Giờ hẹn gặp khách. ISO 8601 KÈM MÚI GIỜ — xem mục 7 |
| guestName | ✅ | Tên khách như trên hộ chiếu |
| guestEmail | Có thì SimPickup gửi thư kèm mã QR cho khách | |
| sendGuestEmail | Đặt false nếu BẠN tự gửi mã QR trong thư của mình | |
| arrivalAt, terminal, pax, note | Tuỳ chọn |
{
"partnerRef": "WEB-2026-00123",
"pickupCode": "ZRT79ZTAUQYV",
"status": "PENDING",
"airportCode":"SGN",
"meetAt": "2026-09-25T01:00:00.000Z",
"qty": 2, "deliveredQty": 0,
"serials": ["8984020001185123945", "8984020001185123896"],
"guestEmailQueued": true,
"replay": false
}
pickupCode là mã khách đưa cho quầy. Nếu bạn tự gửi thư cho khách thì in mã này ra QR: https://app.simpickup.com/p/<pickupCode>
HTTP 409
{ "error": "INSUFFICIENT_STOCK", "requested": 5, "available": 2,
"message": "Kho chỉ còn 2 SIM vinaphone tại sân bay này, đơn cần 5." }
Không con SIM nào bị giữ. Xem GET /stock (mục 8) để biết trước mà đừng bán.
Webhook thanh toán của bạn sẽ gọi lại — mạng timeout, tiến trình chết giữa chừng. partnerRef là thứ giữ cho một lần thanh toán chỉ thành một đơn.
| tình huống | kết quả |
|---|---|
| mã mới | 201 + đơn mới |
| cùng mã, cùng nội dung | 200 + ĐÚNG đơn cũ, "replay": true |
| cùng mã, nội dung khác | 409 PARTNER_REF_CONFLICT |
Cứ gửi lại thoải mái khi không chắc. Đừng bao giờ sinh mã mới cho một lần thanh toán đã có mã — đó là cách duy nhất tạo ra đơn trùng.
Đặt đích webhook trong trang quản trị. Mỗi lần đơn đổi trạng thái, chúng tôi gửi sang:
{
"event": "order.delivered",
"sentAt": "2026-09-25T01:12:00.000Z",
"data": {
"partnerRef": "WEB-2026-00123",
"pickupCode": "ZRT79ZTAUQYV",
"status": "DELIVERED",
"qty": 2, "deliveredQty": 2,
"deliveredAt": "2026-09-25T01:11:58.000Z",
"serials": ["8984...", "8984..."]
}
}
| sự kiện | nghĩa là |
|---|---|
| order.delivered | Khách đã cầm SIM |
| order.partial | Giao THIẾU so với đơn — xem deliveredQty và reason |
| order.cancelled | Đơn bị huỷ, SIM đã về kho |
| order.no_show | Khách không tới |
X-SimPickup-Signature: t=1758765120,v1=6b8a…,v1=91cd…
Ký HMAC-SHA256 trên chuỗi "{t}.{thân tin thô}" bằng bí mật chúng tôi cấp.
const crypto = require('node:crypto');
function hopLe(header, rawBody, secret) {
const p = {};
for (const kv of header.split(',')) {
const [k, v] = kv.trim().split('=');
if (k && v) (p[k] ??= []).push(v);
}
const t = Number(p.t?.[0]);
// Quá 5 phút thì từ chối: chặn kẻ ghi lại tin cũ rồi phát lại.
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const mong = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest();
return (p.v1 ?? []).some((g) => {
const b = Buffer.from(g, 'hex');
return b.length === mong.length && crypto.timingSafeEqual(b, mong);
});
}
Có thể có hai v1. Lúc bạn đổi đích webhook, tin được ký bằng cả bí mật cũ lẫn mới trong 24 giờ để bạn không rớt tin nào. Khớp một trong hai là hợp lệ.
Trả 2xx ngay, xử lý sau — chúng tôi chờ tối đa 10 giây. Trả khác 2xx thì chúng tôi thử lại vài lần có giãn cách. Xử lý nhiều lần cùng một tin phải ra cùng kết quả — thử lại là chuyện thường.
Đừng chỉ dựa vào webhook. Máy chủ bạn sập, hết hạn chứng chỉ, đổi tên miền — tin sẽ không tới, và không bên nào biết hai bên đang lệch nhau.
GET https://app.simpickup.com/api/partner/v1/orders?updatedSince=2026-09-25T00:00:00Z&limit=100
{ "items": [ … ], "nextSince": "2026-09-25T01:11:58.000Z" }
Lưu nextSince, lần sau đưa lại vào updatedSince. Chạy định kỳ vài phút một lần thì dù webhook chết bao lâu bạn cũng không mất đơn nào.
"meetAt": "2026-09-25T08:00:00+07:00" ✅ "meetAt": "2026-09-25T01:00:00Z" ✅ "meetAt": "2026-09-25T08:00:00" ❌ HTTP 400
Thiếu múi giờ thì không ai biết 08:00 là giờ nước nào. Chúng tôi từ chối chứ không đoán — đoán đúng 99 lần rồi sai 1 lần là một khách đứng ở sân bay mà không có ai ra đón, và lần sai đó không phân biệt được với 99 lần kia.
GET https://app.simpickup.com/api/partner/v1/stock
{ "items": [ { "airportCode": "SGN", "carrierCode": "vinaphone", "available": 93 } ] }
Đây là số SIM chưa bị giữ cho đơn nào. Thiếu bước này thì bạn chỉ biết hết hàng sau khi đã cầm tiền của khách.
GET /api/partner/v1/orders/{partnerRef} tra một đơn
POST /api/partner/v1/orders/{partnerRef}/cancel khách hoàn tiền → nhả SIM
GET /api/partner/v1/catalog sân bay + nhà mạng
Huỷ đơn đã giao SIM cho khách thì trả 409 NOT_CANCELLABLE — việc đó phải xử ngoài hệ thống.
Chỉ với khoá sk_test_:
POST /api/partner/v1/sandbox/restock nạp kho thử nghiệm
POST /api/partner/v1/orders/{partnerRef}/sandbox-deliver tự đánh dấu đã giao
restock nạp kho lên 20 SIM cho mỗi sân bay × nhà mạng. Nạp lên trần chứ không cộng dồn, nên bạn cứ bán hết rồi nạp lại bao nhiêu lần tuỳ ý — tiện để chạy thử cả ca INSUFFICIENT_STOCK.
sandbox-deliver đi qua đúng đường giao SIM thật, nên thứ bạn chạy thử chính là thứ sẽ chạy khi lên thật. Nó bắn webhook y như quầy giao thật.
120 lời gọi mỗi phút cho mỗi khoá → 429 kèm Retry-After.
| mã | nghĩa là |
|---|---|
| 400 | Thân tin sai — thiếu trường, sai định dạng, meetAt không có múi giờ |
| 401 | Khoá sai, thiếu, hoặc đã thu hồi |
| 403 | Gọi nhầm sang API nội bộ · dùng sandbox bằng khoá thật · IP_NOT_ALLOWED |
| 404 | Không có đơn nào mang mã đó TRONG PHẠM VI CỦA BẠN |
| 409 | INSUFFICIENT_STOCK · PARTNER_REF_CONFLICT · NOT_CANCELLABLE |
| 429 | Quá tần suất |
Mọi lỗi đều trả JSON kèm message tiếng Việt nói rõ chuyện gì.
Vướng ở đâu thì báo SimPickup kèm partnerRef và giờ gọi — chúng tôi tra được đúng lời gọi đó trong nhật ký. admin@simpickup.com