Đăng nhập lấy khoá
Tài liệu kỹ thuật

API đối tác

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

1 · Xác thực và khoá IP

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/*.

Giữ khoá ở phía máy chủ. Nhét vào mã chạy trên trình duyệt hay ứng dụng điện thoại là đưa khoá cho mọi người — ai mở công cụ nhà phát triển cũng đọc được.

Khoá IP — nên làm

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.

2 · Hai môi trường

sk_test_…sk_live_…
Kho SIMriêng, SIM giả, tự nạp lại đượckho thật của bạn
Đơn hàngriêng, KHÔNG ai ra sân bayquầy SẼ ra sân bay
Đối soát cuối thángkhông tínhcó 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.

3 · Tạo đơn

Khoá chạy thử bắt đầu với kho RỖNG. Gọi POST /api/partner/v1/sandbox/restock một lần trước, nếu không ví dụ dưới đây sẽ trả 409 INSUFFICIENT_STOCK. Xem mục 10.
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ườngbắt buộcghi 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
guestEmailCó 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, noteTuỳ chọn
Đừng gửi số seri SIM. Hệ thống tự chọn và giữ chỗ trong kho của bạn. Khách mua gói cước, không mua một con SIM cụ thể — và hệ thống bên bạn không cần giữ một bản sao kho của mình.

Trả về

{
  "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>

Hết hàng

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.

4 · Gọi lại không được đẻ thêm đơ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ốngkết quả
mã mới201 + đơn mới
cùng mã, cùng nội dung200 + ĐÚNG đơn cũ, "replay": true
cùng mã, nội dung khác409 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.

5 · Nhận tin khi quầy giao xong

Đặ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ệnnghĩa là
order.deliveredKhách đã cầm SIM
order.partialGiao THIẾU so với đơn — xem deliveredQty và reason
order.cancelledĐơn bị huỷ, SIM đã về kho
order.no_showKhách không tới

Kiểm chữ ký — bắt buộc

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);
  });
}
Ký trên thân tin thô, không phải trên object đã JSON.parse rồi stringify lại — thứ tự khoá đổi là chữ ký hỏng.

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ệ.

Quy tắc trả lời

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.

6 · Đường thoát khi webhook chết

Đừ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.

7 · Giờ hẹn phải có múi giờ

"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.

8 · Xem tồn kho trước khi bán

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.

9 · Các endpoint còn lại

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.

10 · Chạy thử trọn vò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.

11 · Giới hạn và mã lỗi

120 lời gọi mỗi phút cho mỗi khoá → 429 kèm Retry-After.

mãnghĩa là
400Thân tin sai — thiếu trường, sai định dạng, meetAt không có múi giờ
401Khoá sai, thiếu, hoặc đã thu hồi
403Gọi nhầm sang API nội bộ · dùng sandbox bằng khoá thật · IP_NOT_ALLOWED
404Không có đơn nào mang mã đó TRONG PHẠM VI CỦA BẠN
409INSUFFICIENT_STOCK · PARTNER_REF_CONFLICT · NOT_CANCELLABLE
429Quá tần suất

Mọi lỗi đều trả JSON kèm message tiếng Việt nói rõ chuyện gì.

12 · Thứ tự nên làm

  1. Lấy khoá sk_test_ trong trang quản trị, khai IP máy chủ của bạn
  2. Đặt đích webhook thử, chép bí mật ký
  3. POST /sandbox/restock → GET /stock xem mã sân bay, mã nhà mạng
  4. POST /orders một đơn, kiểm partnerRef và pickupCode về đúng
  5. Gọi lại đúng lời gọi đó — phải nhận "replay": true, không đẻ đơn thứ hai
  6. sandbox-deliver → kiểm chỗ nhận webhook và xác minh chữ ký
  7. Bán quá số tồn → kiểm bạn xử đúng INSUFFICIENT_STOCK
  8. Dựng vòng dò updatedSince chạy định kỳ
  9. Lấy khoá sk_live_ + khai IP máy chủ chạy thật — không phải sửa mã

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