Lỗi
Mọi response 4xx/5xx đều mang cùng envelope JSON:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" }
]
}code— HTTP status dạng số (400,401,404, …). Hữu ích cho xử lý lớp HTTP chung, nhưng cho logic phân nhánh hãy switch trênmessage—codesẽ không phân biệt được giữa, ví dụ,invalid_inputvàpayment_method_not_supported(cả hai đều 400).message— tên sentinel lower-snake-case, dẫn xuất từ hằng số trongerrors.gonội bộ (ví dụINVALID_INPUT → "invalid_input"). Ổn định qua các bản phát hành — switch trên cái này.details— được điền cho lỗi validation. Mảng các object{ field, message }để client có thể pin lỗi vào input. Bỏ qua trong các trường hợp khác.
Hiện chúng tôi không trả trace_id hay timestamp trong body. Các
bản nháp trước của trang này hứa cả hai — đó là kỳ vọng. Nếu bạn
cần đối soát log server với một request, hãy capture header Date
của response và header rate-limit phía gateway (X-RateLimit-*) và
quote chúng trong support ticket.
Reject ở edge gateway dùng cấu trúc (structure) khác. Envelope ở trên là
cái mà các service backend phát ra. Request bị reject ở gateway
trước khi đến service — X-Signature thiếu/không hợp lệ, X-Client-ID
không được nhận diện, hoặc X-Timestamp cũ trên lệnh gọi /b2b/v1/* — trả
về dưới dạng { "error": "...", "message": "..." }, trong đó
error là slug thô (unauthorized / bad_request /
service_unavailable) và message mang chi tiết. Không có code
dạng số và không có details. Nên một verifier nên phân nhánh
trên HTTP status trước, rồi đọc message, và chỉ coi code/details
là có mặt khi request đã vượt qua gateway. Body gateway mẫu (401):
{ "error": "unauthorized", "message": "invalid signature" }HTTP status → mã điển hình
| HTTP | Các giá trị code điển hình | Ý nghĩa |
|---|---|---|
| 400 | INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUM | Bad request — xem details |
| 401 | INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); INVALID_OTP, OTP_EXPIRED, SESSION_EXPIRED (chỉ dashboard / JWT) | Auth fail — key sai, timestamp hết hạn, chữ ký sai. Mã OTP/SESSION_EXPIRED chỉ surface trên các route dashboard-JWT (/payment/v1/*); tích hợp B2B thuần sẽ không thấy chúng. |
| 402 | INSUFFICIENT_CREDIT | Số dư prepaid của merchant đã hết — top up trước khi retry |
| 403 | FORBIDDEN, IP_BLOCKED | Key hợp lệ nhưng thiếu scope/cấp phép IP cho lệnh gọi này |
| 404 | NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUND | Resource không tồn tại (hoặc không tồn tại cho merchant này) |
| 409 | ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTS | Replay idempotency với body khác, hoặc state machine từ chối transition |
| 429 | TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTS | Đạt rate limit; back off và retry |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROR | Lỗi của chúng tôi; an toàn để retry với backoff. (Các mã cụ thể theo provider như TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR tồn tại nội bộ nhưng chỉ surface trên luồng notification phía dashboard, không phải endpoint B2B.) |
| 503 | SERVICE_UNAVAILABLE | Một dependency downstream đang down. Retry với backoff |
Tham chiếu mã đầy đủ
Toàn bộ các giá trị code bạn có thể thấy (khớp payment-service/pkg/errors/errors.go):
Auth & sessions (401)
INVALID_CREDENTIALS— combo username/password hoặc API key bị rejectINVALID_TOKEN— JWT/session token không parse được hoặc bị tamperTOKEN_EXPIRED— JWT đã quaexpINVALID_OTP— OTP không khớpOTP_EXPIRED— OTP được phát hành cách đây > cửa sổ toleranceSESSION_EXPIRED— session dashboard đã quá tuổiINVALID_SIGNATURE— chữ ký HMAC không khớp trên lệnh gọi B2B / webhook
Authorization (403)
FORBIDDEN— đã xác thực nhưng role/scope/ranh giới merchant chặn hành độngIP_BLOCKED— IP nằm trong danh sách lạm dụng
Not found (404)
NOT_FOUND— chungRECORD_NOT_FOUND— row thiếu cho ID đã choUSER_NOT_FOUND— tra cứu user failSESSION_NOT_FOUND— session id dashboard không nhận diện được
Conflict (409)
ALREADY_EXISTS— chungUSER_ALREADY_EXISTS— signup chạm unique-constraintSESSION_ALREADY_EXISTS— chèn session trùng lặp
Validation (400)
INVALID_INPUT— chung; xemdetailsMISSING_REQUIRED— một trường bắt buộc bị thiếuINVALID_FORMAT— giá trị không khớp định dạng mong đợi (ví dụ UUID, URL, email)INVALID_LENGTH— giá trị quá ngắn hoặc quá dàiINVALID_VALUE— giá trị nằm ngoài enum/range cho phépINVALID_USER_STATUS— user ở trạng thái không cho phép hành độngINVALID_USER_ROLE— role thiếu quyền cho hành độngPAYMENT_METHOD_NOT_SUPPORTED— combo provider/asset chưa được enable cho merchantAMOUNT_BELOW_MINIMUM— amount của order dưới sàn theo từng network hoặc theo cấp env.details.floor_usdtrong response mang sàn cấu hình (USD) để bạn có thể surface trực tiếp; textmessagecũng nêu rõ.INSUFFICIENT_BALANCE— ví của người mua không đủ pay asset để cover giao dịch chuyển.INSUFFICIENT_GAS— ví của người mua thiếu gas gốc để broadcast giao dịch chuyển.
Payment / billing (402)
INSUFFICIENT_CREDIT— số dư prepaid của merchant không thể bù gas / phí platform. Top up qua dashboard, rồi retry
Rate limiting (429)
TOO_MANY_REQUESTS— rate-limit IP của gatewayTOO_MANY_ATTEMPTS— các attempt fail lặp lại trên cùng resource (ví dụ OTP) chạm throttle
Storage / infrastructure (500)
DATABASE_CONNECTION_ERROR— không reach được DBDATABASE_QUERY_ERROR— query plan fail tại runtimeDATABASE_TRANSACTION_ERROR— commit/rollback failREDIS_CONNECTION_ERROR— không reach được RedisREDIS_OPERATION_ERROR— lệnh Redis failEXTERNAL_SERVICE_ERROR— failure bên thứ ba chung (provider không nằm trong nhóm dưới)TWILIO_SERVICE_ERROR— lệnh gọi Twilio SMS / Verify failSENDGRID_SERVICE_ERROR— gửi mail SendGrid failINTERNAL_SERVER_ERROR— fall-through bất ngờ; capture headerDatecủa response +X-RateLimit-*và liên hệ
Availability (503)
SERVICE_UNAVAILABLE— một downstream quan trọng đang báo unhealthy; backoff + retry
Lỗi validation (400)
Khi vấn đề là body request bị lỗi định dạng, details là một mảng để
bạn có thể map lỗi về các trường:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" },
{ "field": "success_url", "message": "must be a valid https URL" }
]
}Lỗi auth (401)
INVALID_SIGNATURE cover ba kiểu fail khác nhau — cách duy nhất để
phân biệt là bisect:
- Sai secret key (kiểm tra lại env var)
- Drift timestamp > 5 phút (đồng bộ NTP)
- Sai chuỗi canonical (thường gặp nhất: quên separator
\n, hoặc ký một body đã được parse rồi stringify lại khác với cái bạn đã gửi)
Xem Xác thực cho thuật toán ký chính xác.
Rate limit (429)
| Surface | Giới hạn |
|---|---|
Mọi route gateway (kể cả /b2b/v1/*) | Theo IP tại gateway — 500 req/min, bucket chung. Không theo merchant. |
Public checkout (/checkout/*) | Sub-bucket chặt hơn — 20 req/min theo IP |
X-RateLimit-Limit và X-RateLimit-Remaining được phát trên mọi
response đi qua bộ rate-limit của gateway (không chỉ khi bị chặn — cả
response thành công cũng có). Coi chúng là budget hiện tại cho IP của bạn.
Response bị rate-limit có include header Retry-After (số giây đến
khi cửa sổ reset) — hãy tôn trọng nó. Như một fallback, back off với
jitter — 1s base + exponential lên 30s.
Rate limit có thể thay đổi. Nếu bạn chạm chúng một cách hợp pháp (ví dụ đối soát một khoảng lịch sử lớn), hãy liên hệ — endpoint bulk nằm trong roadmap.
Thiếu credit (402)
INSUFFICIENT_CREDIT (HTTP 402 Payment Required) nghĩa là số dư
prepaid của merchant không thể bù gas-và-phí-platform tiếp theo cho
thao tác bạn cố thực hiện — thường là settle một thanh toán crypto
hoặc thực thi một hành động on-chain được gas-sponsored. Top up từ
merchant dashboard (Billing → Add credit), rồi retry thao tác;
công việc đang chạy sẽ chờ và tự động pick up khi số dư đủ.
Lỗi server (5xx)
500 nghĩa là chúng tôi không thể xử lý request. Retry với backoff —
idempotency_key của bạn đảm bảo bạn không charge gấp đôi nếu request
ban đầu đã thành công một phần.
Nếu retry không recover trong một phút, surface một “thanh toán tạm
thời không khả dụng” chung cho người mua và liên hệ support với
endpoint fail, merchant ID của bạn, header Date của response và
các giá trị X-RateLimit-*, và thời gian request xấp xỉ — đó là đủ
cho chúng tôi pivot đến request liên quan trong log của mình.
Lỗi delivery webhook
Delivery webhook là một kênh failure riêng — chúng không surface dưới dạng lỗi API vì server của bạn không phải là bên đang gọi. Khi một delivery trả về non-2xx (hoặc timeout), nó được xếp hàng đợi cho retry exponential-backoff tại 0s, 1min, 5min, 15min, 1h, 6h (tổng sáu lần — cùng schedule như Webhooks → Tổng quan). Lịch sử đầy đủ theo từng delivery hiển thị dưới Developers → Webhooks → [endpoint] → Delivery log trong dashboard. Sau lần thứ sáu, delivery bị dead-letter — dashboard surface badge “Failed” mà bạn có thể replay thủ công khi server của bạn khỏe trở lại.