Tài liệu API
Mọi chức năng của panel đều dùng được qua hai API riêng biệt. Cả hai dùng chung tài khoản, số dư và danh sách dịch vụ; điểm khác nhau nằm ở định dạng và tính năng.
Mọi chức năng của panel đều dùng được qua hai API riêng biệt. Cả hai dùng chung tài khoản, số dư và danh sách dịch vụ; điểm khác nhau nằm ở định dạng và tính năng.
Vì sao có hai API?
API đại lý cổ điển (v2) mà cả ngành đang dùng gửi form tới một endpoint duy nhất và luôn trả về HTTP 200. Đó đúng là định dạng mà các phần mềm panel dựng sẵn mong đợi, nên chúng tôi giữ nguyên. Còn lập trình viên tự xây hệ thống thì liên tục vướng giới hạn của nó: không phân biệt được các loại lỗi, danh sách dịch vụ trả về nguyên một khối, và phải gọi kiểm tra trạng thái đơn hàng liên tục không dứt. v3 ra đời chính là dành cho họ.
So sánh
| Tiêu chí | API cũ (v2) | API mới (v3) |
|---|---|---|
| Kiểu thiết kế | Một endpoint, gửi form, tham số action | REST theo tài nguyên, body JSON |
| Mã trạng thái HTTP | Luôn là 200, kể cả khi lỗi | Mã thật (400, 401, 402, 404, 409, 429, 502) |
| Lỗi | Văn bản tự do | type + code cố định + thông báo theo ngôn ngữ + param + doc_url |
| Trạng thái đơn hàng | Chỉ có văn bản theo ngôn ngữ | Giá trị cố định cho máy xử lý, kèm nhãn hiển thị riêng |
| Mô tả dịch vụ | Không có | Mô tả bằng 16 ngôn ngữ, thời gian trung bình, nền tảng, danh mục |
| Trường của đơn hàng | Đoán từ tên loại dịch vụ | Mỗi dịch vụ công bố schema trường riêng |
| Đơn vị tính giá | Không ghi rõ (dễ gây sai lệch 1.000 lần với dịch vụ gói) | Ghi rõ per_1000 hoặc per_order |
| Danh sách dịch vụ | Mọi dịch vụ trong một response | Bộ lọc kèm phân trang bằng cursor |
| Chống đơn trùng | Không có | Idempotency-Key |
| Cập nhật trạng thái | Gọi kiểm tra liên tục | Webhook có chữ ký hoặc luồng sự kiện |
| Schema | Không có | OpenAPI 3.1 |
| Ngôn ngữ | Tiếng Anh và tiếng Thổ Nhĩ Kỳ (URL riêng) | 16 ngôn ngữ (qua header hoặc tham số) |
| Chất lượng dịch vụ | Không có | Điểm, độ tin cậy và bằng chứng cho từng dịch vụ; danh sách xếp hạng |
Nên dùng API nào?
Chọn API cũ nếu bạn dùng phần mềm panel dựng sẵn, bot hoặc panel đại lý. Phần lớn chỉ yêu cầu đổi URL API và key, vài phút sau là chạy được.
Chọn v3 nếu bạn tự viết ứng dụng, cửa hàng hoặc công cụ tự động hóa. Xử lý lỗi, chống đơn trùng và thông báo đều có sẵn; bạn còn có thể dựng form đặt hàng thẳng từ schema của dịch vụ.
Bắt đầu
- 1Tạo API key ở tab API key.
- 2Lấy danh sách dịch vụ, đọc id và schema trường của dịch vụ bạn cần.
- 3Kiểm tra đơn hàng bằng preview trước, rồi mới tạo đơn.
- 4Đăng ký webhook hoặc đọc luồng sự kiện để nắm được các thay đổi trạng thái.
REST API dành cho lập trình viên tự xây hệ thống: đường dẫn theo tài nguyên, mã trạng thái HTTP thật, lỗi mà máy đọc được và thông báo có chữ ký.
URL gốc
Mọi đường dẫn đều được nối vào sau URL này. Phiên bản nằm ngay trong đường dẫn: nếu sau này cần một thay đổi phá vỡ tương thích, chúng tôi công bố đường dẫn mới (v4) và đường dẫn hiện tại vẫn chạy nguyên vẹn. Ngày phát hành của đặc tả API (API contract) được trả về trong header X-Api-Version ở mọi response.
https://panelfollows.com/api/v3Xác thực
Gửi API key dưới dạng Bearer token trong header Authorization. Bạn cũng có thể dùng header X-Api-Key để thay thế.
GET https://panelfollows.com/api/v3/account
Authorization: Bearer pf_live_...Key cũ bạn đang có cũng dùng được trên v3, nên bạn có thể thử ngay. Khi chạy thật, hãy dùng key v3: key này đặt được nhãn, thu hồi riêng lẻ và không bao giờ được lưu dưới dạng văn bản thuần.
Bắt đầu nhanh
curl https://panelfollows.com/api/v3/services?limit=5 \
-H "Authorization: Bearer YOUR_API_KEY"Ngôn ngữ
Chọn ngôn ngữ của response bằng header Accept-Language hoặc tham số ?lang=; khi có cả hai, tham số được ưu tiên. Tên dịch vụ, mô tả dịch vụ, tên danh mục, nhãn trạng thái đơn hàng, nhãn trường đặt hàng và thông báo lỗi đều trả về bằng ngôn ngữ đó.
Các giá trị dành cho máy không bao giờ đổi theo ngôn ngữ: error.code, order.status, service.type và currency luôn giữ nguyên. Hãy rẽ nhánh theo các giá trị này, còn phần văn bản thì hiển thị cho người dùng của bạn.
Accept-Language: tr
# veya
GET https://panelfollows.com/api/v3/services?lang=trĐịnh dạng request và response
Body của request là JSON (application/json); form-urlencoded cũng được chấp nhận để thử nhanh. Response là JSON: một tài nguyên đơn lẻ là một object thường, còn danh sách được bọc trong một envelope gồm data, has_more và next_cursor. Mọi object đều có trường object cho biết kiểu của nó.
Số tiền là CHUỖI thập phân ("1.2340"), không phải số thực dấu phẩy động. Hãy parse chúng sang kiểu decimal ở phía bạn để không mất phần lẻ. Đơn vị tiền tệ là USD.
Mốc thời gian theo chuẩn RFC 3339 (2026-08-21T00:24:45.255Z).
Lỗi
Khi thất bại, API trả về mã trạng thái HTTP thật cùng một body chứa đúng một object error. Code của bạn nên rẽ nhánh theo error.code: giá trị này cố định và không đổi theo ngôn ngữ.
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: req_0c858d8af7f65eca001b2f5a
{
"object": "error",
"error": {
"type": "invalid_request_error",
"code": "quantity_out_of_range",
"message": "Miktar, bu servisin izin verdiği aralığın dışında.",
"param": "quantity",
"doc_url": "https://panelfollows.com/api-docs#error-quantity_out_of_range",
"request_id": "req_0c858d8af7f65eca001b2f5a"
}
}| type | Nhóm lỗi chung: có thử lại được không, có phải lỗi từ phía bạn không. |
| code | Giá trị cố định dành cho máy. Hãy rẽ nhánh theo trường này. |
| message | Văn bản cho người đọc, bằng ngôn ngữ bạn đã chọn. |
| param | Tên trường gây lỗi, nếu có. |
| doc_url | Link tới đúng mục liên quan trong tài liệu này. |
| request_id | Mã tham chiếu duy nhất cần cung cấp khi liên hệ hỗ trợ. |
Giới hạn tần suất
600 request mỗi phút cho mỗi key, cộng thêm 900 request mỗi phút cho mỗi IP. Mọi response đều kèm RateLimit-Limit, RateLimit-Remaining và RateLimit-Reset để bạn tự giảm tốc trước khi chạm ngưỡng. Khi vượt giới hạn, bạn sẽ nhận mã 429 kèm header Retry-After.
Phân trang
Danh sách được phân trang bằng cursor. Gửi limit cho kích thước trang (tối đa 500) và starting_after là id của phần tử cuối cùng ở trang trước. Cứ tiếp tục cho đến khi has_more là false; next_cursor cho bạn cursor của lần gọi kế tiếp. Giá trị limit ngoài phạm vi không bị âm thầm cắt bớt mà sẽ báo lỗi: việc âm thầm cắt bớt khiến client tưởng mình đã lấy hết dữ liệu.
Chống đơn trùng (Idempotency-Key)
Thêm header Idempotency-Key ngẫu nhiên khi tạo đơn hàng. Nếu mất kết nối và bạn gửi lại với cùng key đó, sẽ không có đơn thứ hai nào được tạo: response đầu tiên được trả lại, kèm header Idempotent-Replay: true. Bản ghi được lưu trong 24 giờ.
Gửi cùng một key với body KHÁC sẽ nhận lỗi 409 idempotency_key_reuse. Điều này gần như luôn cho thấy cách sinh key ở phía client đang có lỗi. Request thất bại không làm key mất hiệu lực: hãy sửa lỗi rồi gửi lại với chính key đó.
Tạo đơn hàng
curl -X POST https://panelfollows.com/api/v3/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ea3a-4b1c-9c1e-2b0d5c6a7e91" \
-d '{
"service": 1234,
"link": "https://instagram.com/username",
"quantity": 1000
}'Schema trường của dịch vụ
Mỗi dịch vụ công bố các trường nó cần trong mảng fields: tên, kiểu, có bắt buộc hay không, các giới hạn, cùng nhãn và mô tả bằng ngôn ngữ của bạn. Khi một trường có determines_quantity là true, số lượng được tính từ số dòng trong trường đó.
// Servisin kendi alan şemasından formu OTOMATİK üretmek:
// hiçbir servis tipini koda gömmeniz gerekmez.
const res = await fetch("https://panelfollows.com/api/v3/services/1234", {
headers: { Authorization: "Bearer YOUR_API_KEY", "Accept-Language": "tr" },
});
const service = await res.json();
for (const field of service.fields) {
renderInput({
name: field.name,
label: field.label, // kullanıcının dilinde
hint: field.description, // kullanıcının dilinde
required: field.required,
type: field.type, // url | integer | string | text_lines
min: field.min,
max: field.max,
// true ise miktarı bu alanın satır sayısı belirler
countsLines: field.determines_quantity === true,
});
}Object dịch vụ
{
"object": "service",
"id": 1234,
"name": "Instagram Takipçi | Türk | 30 gün telafi",
"description": "Gerçek hesaplardan Türk takipçi. Başlangıç 0-1 saat.",
"type": "default",
"platform": "instagram",
"category": { "slug": "instagram-takipci", "name": "Instagram Takipçi" },
"pricing": {
"rate": "1.2340",
"currency": "USD",
"unit": "per_1000",
"unit_note": "Fiyat 1000 adet içindir."
},
"limits": { "min": 100, "max": 100000 },
"features": { "refill": true, "cancel": false, "dripfeed": true },
"average_time_seconds": 4320,
"fields": [
{
"name": "link",
"type": "url",
"required": true,
"label": "Bağlantı",
"description": "Gönderimin yapılacağı profilin herkese açık adresi."
},
{
"name": "quantity",
"type": "integer",
"required": true,
"label": "Miktar",
"description": "Kaç adet gönderileceği.",
"min": 100,
"max": 100000
}
],
"is_active": true,
"updated_at": "2026-08-20T09:15:00.000Z"
}Xem trước đơn hàng (chạy thử)
POST /orders/preview kiểm tra đơn hàng và tính số tiền phải trả mà KHÔNG tạo đơn. Bạn có thể hiển thị giá cho khách và kiểm tra số dư có đủ hay không từ trước. Không có khoản nào bị trừ và hệ thống cũng không liên hệ nhà cung cấp nào.
Đặt hàng loạt
POST /orders/batch nhận tối đa 50 đơn trong một lần gọi. Các mục được xử lý theo thứ tự và mỗi mục trả về kết quả riêng: nếu một mục lỗi, các mục còn lại vẫn được tạo, và bạn thấy rõ mục nào lỗi và vì sao.
Nhúng object liên quan
Truyền include=service ở các endpoint đơn hàng, object dịch vụ sẽ được nhúng sẵn vào response, bạn khỏi phải gửi thêm một request.
Chất lượng dịch vụ và danh sách dịch vụ tốt nhất
Mỗi giờ panel đo lại từng dịch vụ: các đơn của chính chúng tôi cho dịch vụ đó kết thúc ra sao (hoàn thành, bị hủy, bị kẹt, bị từ chối), khách hàng yêu cầu bảo hành hoặc mở ticket thường xuyên đến mức nào, thời gian giao thực tế là bao lâu, và nguồn còn liệt kê dịch vụ đó hay không. GET /services/top biến các số đo này thành một danh sách xếp hạng, còn include=quality gắn cùng báo cáo đó vào bất kỳ object dịch vụ nào.
Điểm (0-100) là tổng có trọng số của năm thành phần: độ ổn định (42%, tỷ lệ thành công ước tính), mức hài lòng (14%, tính từ tỷ lệ khiếu nại), tốc độ (28%, theo thang logarit của thời gian giao thực tế; 5 phút đạt điểm tối đa, 48 giờ được 0 điểm), điểm riêng của hệ thống giám sát sức khỏe dịch vụ (8%) và lượng bằng chứng (8%). Dịch vụ được cộng thêm một ít điểm nếu có bảo hành, đến từ nguồn đáng tin cậy hoặc đã có mặt lâu trong danh sách dịch vụ. Dịch vụ đang được xem xét, bị hệ thống giám sát gắn cờ hoặc đến từ nguồn đang trong thời gian theo dõi vẫn được chấm điểm nhưng không bao giờ được xếp hạng.
Lọc theo nền tảng, slug danh mục hoặc nhóm dịch vụ (followers, likes, views... dùng chung cho mọi nền tảng), sắp xếp theo score, speed, reliability, price hoặc orders, và dùng group_by=category (hoặc platform) cùng per_group để lấy mỗi danh mục một danh sách chỉ trong một lần gọi: đúng thứ một cửa hàng cần để đánh dấu dịch vụ 'đề xuất' trong từng danh mục.
# Instagram takipçi kategorisinde en iyi 5 servis
curl "https://panelfollows.com/api/v3/services/top?category=instagram-followers&limit=5" -H "Authorization: Bearer YOUR_API_KEY"
# Her kategori için kanıtlı en iyi 3 servis (tek istek)
curl "https://panelfollows.com/api/v3/services/top?group_by=category&per_group=3&min_confidence=medium" -H "Authorization: Bearer YOUR_API_KEY"| score / grade | 0-100 và A (85+), B (70+), C (55+), D. |
| confidence | none, low, medium, high: số đơn của chính chúng tôi làm cơ sở cho điểm. |
| badges | proven (5+ đơn, cận dưới tỷ lệ thành công từ 60%), popular (20+ đơn), fast (giao trong vòng 1 giờ), trusted_source, new (dưới 14 ngày). |
| components | reliability, satisfaction, speed, health, evidence, mỗi giá trị từ 0 đến 1. |
| evidence.basis | service_orders (đơn của chính dịch vụ), peer_services (bắt đầu từ các dịch vụ khác của cùng nguồn) hoặc none. |
| evidence.delivery_source | measured (trung vị do chúng tôi đo trên 3+ đơn đã hoàn thành) hoặc claimed (thời gian nguồn tự công bố, bị trừ điểm và giới hạn trần). |
| measured_at | Thời điểm hệ thống giám sát đo dịch vụ lần gần nhất; điểm được làm mới mỗi giờ. |
Tham chiếu endpoint
| Phương thức | Endpoint | Mô tả |
|---|---|---|
| GET | /api/v3 | Discovery document: version, endpoints, limits and event types. |
| GET | /api/v3/openapi.json | OpenAPI 3.1 schema for this API. |
| GET | /api/v3/account | Account balance, currency and current rate-limit window. |
| PATCH | /api/v3/account | Set the low balance alert threshold that triggers account.low_balance. |
| GET | /api/v3/services | List services with filters and cursor pagination. |
| GET | /api/v3/services/top | Best services ranked by the quality score; optionally one shortlist per category or platform. |
| GET | /api/v3/services/{id} | Retrieve one service, including its order field schema. |
| GET | /api/v3/categories | List categories with platform, shelf and active service counts. |
| GET | /api/v3/platforms | List platform keys usable as the ?platform= filter, with counts. |
| POST | /api/v3/orders | Create an order. Supports the Idempotency-Key header. |
| POST | /api/v3/orders/preview | Validate an order and compute its charge without creating it. |
| POST | /api/v3/orders/batch | Create up to 50 orders in one call; each item reports its own result. |
| GET | /api/v3/orders | List your orders, newest first. |
| GET | /api/v3/orders/{id} | Retrieve one order. |
| POST | /api/v3/orders/{id}/cancel | Request cancellation. Only for services whose features.cancel is true. |
| POST | /api/v3/orders/{id}/refill | Request a refill for a completed order. |
| GET | /api/v3/refills | List your refill requests, newest first. |
| GET | /api/v3/refills/{id} | Retrieve one refill; refreshes its status from the provider. |
| GET | /api/v3/events | Read your event stream oldest-first; the polling alternative to webhooks. |
| GET | /api/v3/webhooks | List your webhook endpoints. |
| POST | /api/v3/webhooks | Register a webhook endpoint. The signing secret is returned once. |
| GET | /api/v3/webhooks/{id} | Retrieve one webhook endpoint. |
| PATCH | /api/v3/webhooks/{id} | Update a webhook endpoint's url, events, description or active state. |
| DELETE | /api/v3/webhooks/{id} | Delete a webhook endpoint and its delivery log. |
| POST | /api/v3/webhooks/{id}/test | Send a test event to this endpoint, ignoring its event filter. |
| POST | /api/v3/webhooks/{id}/rotate_secret | Generate a new signing secret. The old one stops working immediately. |
| GET | /api/v3/webhooks/{id}/deliveries | Delivery log for one endpoint: attempts, response codes and errors. |
Schema OpenAPI
Định nghĩa toàn bộ endpoint ở dạng máy đọc được. Nạp file này vào công cụ sinh client (openapi-generator, Kiota) hoặc Postman là bạn có ngay một client viết sẵn bằng ngôn ngữ lập trình bạn dùng.
https://panelfollows.com/api/v3/openapi.jsonCâu hỏi thường gặp
Key cũ của tôi có dùng được với v3 không?
Có. Bạn không cần key mới để thử. Dù vậy, khi chạy thật nên chuyển sang key v3: key này thu hồi được và không được lưu dưới dạng văn bản thuần.
Vì sao giá được trả về dạng chuỗi?
Số thực dấu phẩy động làm mất phần lẻ khi biểu diễn số tiền thập phân. Trả về chuỗi rồi để bạn parse sang kiểu decimal giúp loại bỏ hẳn nhóm sai lệch làm tròn này.
Vì sao trường unit lại quan trọng?
Phần lớn dịch vụ tính giá theo mỗi 1.000 đơn vị (per_1000), nhưng dịch vụ dạng gói được bán như một món duy nhất (per_order), giá đã bao trọn cả gói. Những tích hợp bỏ qua khác biệt này đã tính giá gói lệch tới 1.000 lần.
Vì sao đơn hàng đã tạo nhưng vẫn ở trạng thái pending?
Việc chuyển đơn sang nhà cung cấp có thể bị chậm. Số tiền của bạn đã được tạm giữ và đơn hàng không bị mất; đội ngũ của chúng tôi sẽ tự động gửi lại. Trường processing_delayed đánh dấu trạng thái này.
Tất cả dịch vụ đều hỗ trợ hủy đơn và bảo hành phải không?
Không. Hãy kiểm tra features.cancel và features.refill trong object dịch vụ. Gọi endpoint hủy đơn hoặc bảo hành cho dịch vụ không hỗ trợ sẽ nhận mã 400.
Có dùng song song cả hai API được không?
Được. Cùng tài khoản, cùng số dư, cùng đơn hàng. Đơn đặt qua v2 có thể đọc lại qua v3.
API đại lý cổ điển, là chuẩn chung của cả ngành. Đây là định dạng mà các phần mềm panel dựng sẵn mong đợi.
Endpoint
POST https://panelfollows.com/api/v2
POST https://panelfollows.com/api/v2/trXác thực
Mọi request đều mang tham số key. Hãy giữ key bí mật và tạo lại ngay nếu bị lộ.
Định dạng request và response
Request được gửi bằng POST dưới dạng form (application/x-www-form-urlencoded), response là JSON. Khi lỗi, API vẫn trả về HTTP 200, kèm { "error": "..." } trong body.
Ngoài ra còn hỗ trợ hai cách trước đây chưa từng được ghi vào tài liệu: gọi bằng GET, và gửi body dưới dạng application/json.
Giới hạn tần suất
240 request mỗi phút cho mỗi key, cộng thêm 300 request mỗi phút cho mỗi IP. Request vượt giới hạn bị từ chối với mã 429.
Action và tham số
| action | Tham số | Mô tả |
|---|---|---|
| services | key, action | Liệt kê mọi dịch vụ đang hoạt động (id, tên, danh mục, giá, min/max, bảo hành, hủy đơn, drip-feed). |
| add | key, action, service, link, quantity[, runs, interval, comments, username, posts, min, max] | Tạo đơn hàng. service là id dịch vụ trong danh sách. Thêm runs và interval cho drip-feed, cùng các trường tương ứng với loại dịch vụ đặc biệt. |
| status | key, action, order | orders | Trạng thái đơn hàng. Dùng order cho một đơn, hoặc danh sách orders phân tách bằng dấu phẩy cho nhiều đơn. |
| balance | key, action | Số dư tài khoản và đơn vị tiền tệ. |
| refill | key, action, order | orders | Tạo yêu cầu bảo hành, gửi thẳng tới nhà cung cấp. |
| refill_status | key, action, refill | refills | Kiểm tra trạng thái bảo hành. |
| cancel | key, action, orders | Hủy đơn hàng. Chỉ áp dụng cho dịch vụ có nhà cung cấp hỗ trợ hủy đơn. |
Ví dụ
curl -X POST https://panelfollows.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=add" \
-d "service=1234" \
-d "link=https://instagram.com/username" \
-d "quantity=1000"
# Yanıt: { "order": 23501 }Response bằng tiếng Thổ Nhĩ Kỳ
Thêm /tr vào cuối URL để nhận tên dịch vụ, danh mục, trạng thái đơn hàng và thông báo lỗi bằng tiếng Thổ Nhĩ Kỳ. Tham số, action và cấu trúc response giữ nguyên, và key của bạn dùng được trên cả hai URL. Các trường kỹ thuật (type, refill_status, currency) vẫn để tiếng Anh để tương thích với chuẩn chung.
Chuyển sang v3
Việc chuyển đổi là không bắt buộc. Nếu chuyển, phần lớn logic nghiệp vụ của bạn vẫn giữ được vì tên tham số không đổi; khác biệt chỉ nằm ở cách truyền request và cách đọc lỗi.
- 1Chuyển key từ trường key trong body sang header Authorization: Bearer.
- 2Gọi đường dẫn tài nguyên thay cho action=... (POST /orders thay cho add).
- 3Xác định lỗi bằng mã trạng thái HTTP và error.code thay vì kiểm tra "có trường error hay không".
- 4So sánh trạng thái đơn hàng với giá trị dành cho máy, không so với văn bản hiển thị.
- 5Thêm Idempotency-Key khi tạo đơn hàng.
- 6Thay việc gọi kiểm tra trạng thái liên tục bằng webhook.
Key cấp toàn quyền truy cập tài khoản của bạn. Không chia sẻ key, không nhúng key vào code chạy phía client và tuyệt đối không commit key lên repository công khai.
Key v3
Tạo bao nhiêu key tùy nhu cầu, đặt nhãn cho từng key và thu hồi riêng lẻ từng key. Phía chúng tôi chỉ lưu bản băm mật mã (digest) của key.
Tạo tài khoản miễn phíKey cũ
Key duy nhất mà API đại lý cổ điển (v2) sử dụng. Key này cũng dùng được trên v3. Khi tạo lại, giá trị cũ mất hiệu lực ngay lập tức.
Bảo mật
- Lưu key trong biến môi trường, không bao giờ để trong mã nguồn.
- Không đặt key trong code chạy trên trình duyệt; hãy gọi API thông qua máy chủ của chính bạn.
- Tạo key riêng cho từng hệ thống để khi thu hồi một key, các hệ thống khác không bị ảnh hưởng.
- Nếu nghi key bị lộ, hãy triển khai key mới trước rồi mới thu hồi key cũ.
Khi đơn hàng đổi trạng thái, chúng tôi gửi một thông báo có chữ ký tới máy chủ của bạn, nên bạn không bao giờ phải liên tục gọi kiểm tra trạng thái.
Vì sao nên dùng webhook?
Gọi kiểm tra liên tục vừa chậm vừa lãng phí: hỏi về hàng nghìn đơn mỗi phút sẽ ngốn hết giới hạn tần suất, mà bạn vẫn biết thay đổi trễ vài phút. Với webhook, thay đổi đến tay bạn ngay khi nó xảy ra.
Thiết lập
- 1Chuẩn bị một URL https công khai (địa chỉ local và địa chỉ mạng riêng sẽ bị từ chối).
- 2Thêm URL ở bên dưới và lưu lại signing secret (chỉ hiển thị một lần).
- 3Xác minh chữ ký ở phía bạn và trả về mã 2xx.
- 4Dùng nút gửi thử để kiểm tra toàn bộ luồng từ đầu đến cuối.
Nội dung chúng tôi gửi
POST /hooks/pf HTTP/1.1
Content-Type: application/json
Webhook-Id: evt_7f910fba7cd042ef9d9069ba5c074fa0
Webhook-Timestamp: 1787261223
Webhook-Signature: t=1787261223,v1=9c1e2b0d5c6a7e91...
{
"object": "event",
"id": "evt_7f910fba7cd042ef9d9069ba5c074fa0",
"type": "order.completed",
"created_at": "2026-08-21T00:27:03.531Z",
"data": {
"previous_status": "in_progress",
"order": {
"object": "order",
"id": 23501,
"status": "completed",
"status_label": "Tamamlandı",
"service": 1234,
"quantity": 1000,
"start_count": 4210,
"remains": 0,
"charge": "1.2340",
"currency": "USD"
}
}
}Xác minh chữ ký
Mọi request đều mang header Webhook-Signature: t là mốc thời gian, v1 là chữ ký. Chữ ký là HMAC-SHA256 của chuỗi "<timestamp>.<raw body>", tính bằng secret của bạn.
- 1Tách t và v1 ra khỏi header.
- 2Kiểm tra t không cũ hơn 5 phút để chặn tấn công phát lại (replay).
- 3Tính HMAC-SHA256 của "<t>.<raw body>" bằng secret của bạn.
- 4So sánh với v1 bằng phép so sánh constant-time (thời gian không đổi) và từ chối request nếu không khớp.
Ví dụ xác minh
import crypto from "node:crypto";
import express from "express";
const app = express();
// ÖNEMLİ: imza HAM gövde üzerinden hesaplanır. JSON'a çevirip yeniden
// dizeye dönüştürürseniz boşluklar değişir ve imza tutmaz.
app.post("/hooks/pf", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const header = req.get("Webhook-Signature") ?? "";
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header);
if (!m) return res.sendStatus(400);
const [, timestamp, signature] = m;
// Tekrar saldırısına karşı: 5 dakikadan eski damgayı reddet.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${raw}`, "utf8")
.digest("hex");
const ok =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(raw);
// 2xx dönmezseniz gönderim artan aralıklarla tekrar denenir.
res.sendStatus(200);
if (event.type === "order.completed") {
// ... siparişi kendi sisteminizde tamamlandı olarak işaretleyin
}
});Gửi lại
Lần gửi đầu tiên diễn ra ngay khi sự kiện xảy ra. Nếu response không phải 2xx hoặc kết nối thất bại, hệ thống sẽ gửi lại sau 1 phút, 5 phút, 30 phút, 2 giờ và 6 giờ. Sau 6 lần thử, lượt gửi được đánh dấu thất bại và xuất hiện trong nhật ký gửi.
Loại sự kiện
Đăng ký nhận những loại bạn quan tâm, hoặc nhận tất cả. Mỗi lần đổi trạng thái sinh ra đúng một sự kiện, thuộc loại khớp nhất với trạng thái mới.
| order.created | Đơn hàng đã được tạo. |
| order.processing | Nhà cung cấp đã bắt đầu chạy đơn. |
| order.completed | Đơn hàng đã hoàn thành. |
| order.partial | Đơn hàng chỉ giao được một phần, phần còn lại đã được hoàn tiền. |
| order.canceled | Đơn hàng đã bị hủy hoặc đã được hoàn tiền. |
| order.updated | Trạng thái thay đổi theo cách khác. |
| refill.created | Có yêu cầu bảo hành mới. |
| refill.updated | Yêu cầu bảo hành đã đổi trạng thái. |
| account.low_balance | Số dư của bạn đã xuống dưới ngưỡng bạn đặt bằng PATCH /account. Sự kiện chỉ phát đúng lúc số dư giảm xuống dưới ngưỡng, không lặp lại ở mỗi đơn, và sẽ sẵn sàng phát lần nữa sau khi số dư trở lại trên ngưỡng. |
Nếu bạn không thể dựng webhook
Các sự kiện này cũng đọc được bằng cursor qua GET /api/v3/events. Hãy dùng cách này khi phát triển trên máy cá nhân, khi không có IP tĩnh hoặc khi hệ thống nằm sau tường lửa.
// Webhook kuramıyorsanız (yerelde geliştirme, sabit IP yok) aynı bilgiyi
// imleçle çekebilirsiniz. İmleci kendi tarafınızda saklayın.
let cursor = loadCursor(); // en son işlediğiniz olayın "cursor" değeri
const url = new URL("https://panelfollows.com/api/v3/events");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", String(cursor));
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
const { data, has_more } = await res.json();
for (const event of data) {
handle(event); // sizin işleyiciniz
cursor = event.cursor; // imleci ilerlet
}
saveCursor(cursor);Lỗi (48)
| Mã | Trạng thái | Mô tả |
|---|---|---|
| missing_api_key | 401 | Chưa cung cấp API key. Vui lòng gửi key theo dạng 'Authorization: Bearer <key>'. |
| invalid_api_key | 401 | API key bạn cung cấp không hợp lệ. |
| revoked_api_key | 401 | API key này đã bị thu hồi và không thể dùng được nữa. |
| account_banned | 403 | Tài khoản này đã bị cấm. |
| account_suspended | 403 | Tài khoản này đang bị tạm khóa. |
| insufficient_scope | 403 | API key này không có quyền truy cập endpoint này. |
| invalid_json | 400 | Body của request không phải JSON hợp lệ. |
| unsupported_content_type | 415 | Content-Type không được hỗ trợ. Vui lòng dùng application/json hoặc application/x-www-form-urlencoded. |
| method_not_allowed | 405 | Endpoint này không cho phép phương thức HTTP này. |
| payload_too_large | 413 | Body của request quá lớn. |
| missing_parameter | 400 | Thiếu một tham số bắt buộc. |
| invalid_parameter | 400 | Một tham số có giá trị không hợp lệ. |
| invalid_link | 400 | Link bị thiếu hoặc không phải URL http(s) hợp lệ. |
| invalid_quantity | 400 | Số lượng không phải số nguyên dương hợp lệ. |
| quantity_out_of_range | 400 | Số lượng nằm ngoài phạm vi cho phép của dịch vụ này. |
| invalid_comments | 400 | Trường bình luận đang trống hoặc có quá nhiều dòng. |
| invalid_username | 400 | Tên người dùng không hợp lệ với dịch vụ này. |
| invalid_subscription | 400 | Tham số của gói tự động không hợp lệ. |
| invalid_runs | 400 | Giá trị 'runs' không hợp lệ cho drip-feed. |
| invalid_interval | 400 | Giá trị 'interval' không hợp lệ cho drip-feed. |
| dripfeed_not_supported | 400 | Dịch vụ này không hỗ trợ drip-feed. |
| missing_required_field | 400 | Thiếu một trường bắt buộc của loại dịch vụ này, hoặc trường đó không hợp lệ. |
| service_inactive | 400 | Dịch vụ này hiện không nhận đơn. |
| invalid_cursor | 400 | Cursor phân trang không hợp lệ. |
| invalid_limit | 400 | Tham số 'limit' nằm ngoài phạm vi cho phép. |
| invalid_webhook_url | 400 | URL webhook phải là một địa chỉ https:// công khai. |
| invalid_events | 400 | Một hoặc nhiều loại sự kiện được yêu cầu không xác định. |
| batch_too_large | 400 | Request hàng loạt có quá nhiều mục. |
| cancel_not_supported | 400 | Dịch vụ này không hỗ trợ hủy đơn. |
| refill_not_supported | 400 | Dịch vụ này không có bảo hành. |
| unknown_endpoint | 404 | Endpoint không tồn tại. Xem tài liệu tham chiếu API để biết các đường dẫn hiện có. |
| service_not_found | 404 | Không có dịch vụ nào với id này. |
| order_not_found | 404 | Tài khoản của bạn không có đơn hàng nào với id này. |
| refill_not_found | 404 | Tài khoản của bạn không có yêu cầu bảo hành nào với id này. |
| webhook_not_found | 404 | Tài khoản của bạn không có endpoint webhook nào với id này. |
| order_not_cancelable | 409 | Không thể hủy đơn hàng này nữa do trạng thái hiện tại của đơn. |
| cancel_rejected | 409 | Nhà cung cấp đã từ chối yêu cầu hủy đơn. |
| order_not_completed | 409 | Chỉ có thể yêu cầu bảo hành cho đơn hàng đã hoàn thành. |
| duplicate_link | 409 | Link này đang có một đơn hàng chưa hoàn tất. Vui lòng chờ đơn đó hoàn thành. |
| idempotency_key_reuse | 409 | Idempotency-Key này đã được dùng với một body request khác. |
| idempotency_in_progress | 409 | Request với Idempotency-Key này vẫn đang được xử lý. Vui lòng thử lại sau giây lát. |
| webhook_limit_reached | 409 | Bạn đã đạt số lượng endpoint webhook tối đa. |
| insufficient_balance | 402 | Số dư không đủ cho đơn hàng này. |
| rate_limit_exceeded | 429 | Đã vượt giới hạn tần suất. Xem header Retry-After trong response. |
| provider_error | 502 | Nhà cung cấp phía trên (upstream) trả về lỗi. Vui lòng thử lại. |
| refill_failed | 502 | Nhà cung cấp đã từ chối yêu cầu bảo hành. |
| service_temporarily_unavailable | 503 | Dịch vụ này tạm thời không khả dụng. Vui lòng thử lại sau. |
| internal_error | 500 | Đã xảy ra lỗi ngoài dự kiến ở phía chúng tôi. |