Skip to Content

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" } ] }
  • codeHTTP 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ên messagecode sẽ không phân biệt được giữa, ví dụ, invalid_inputpayment_method_not_supported (cả hai đều 400).
  • message — tên sentinel lower-snake-case, dẫn xuất từ hằng số trong errors.go nộ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

HTTPCác giá trị code điển hìnhÝ nghĩa
400INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUMBad request — xem details
401INVALID_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.
402INSUFFICIENT_CREDITSố dư prepaid của merchant đã hết — top up trước khi retry
403FORBIDDEN, IP_BLOCKEDKey hợp lệ nhưng thiếu scope/cấp phép IP cho lệnh gọi này
404NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUNDResource không tồn tại (hoặc không tồn tại cho merchant này)
409ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTSReplay idempotency với body khác, hoặc state machine từ chối transition
429TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTSĐạt rate limit; back off và retry
500INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERRORLỗ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.)
503SERVICE_UNAVAILABLEMộ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ị reject
  • INVALID_TOKEN — JWT/session token không parse được hoặc bị tamper
  • TOKEN_EXPIRED — JWT đã qua exp
  • INVALID_OTP — OTP không khớp
  • OTP_EXPIRED — OTP được phát hành cách đây > cửa sổ tolerance
  • SESSION_EXPIRED — session dashboard đã quá tuổi
  • INVALID_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 động
  • IP_BLOCKED — IP nằm trong danh sách lạm dụng

Not found (404)

  • NOT_FOUND — chung
  • RECORD_NOT_FOUND — row thiếu cho ID đã cho
  • USER_NOT_FOUND — tra cứu user fail
  • SESSION_NOT_FOUND — session id dashboard không nhận diện được

Conflict (409)

  • ALREADY_EXISTS — chung
  • USER_ALREADY_EXISTS — signup chạm unique-constraint
  • SESSION_ALREADY_EXISTS — chèn session trùng lặp

Validation (400)

  • INVALID_INPUT — chung; xem details
  • MISSING_REQUIRED — một trường bắt buộc bị thiếu
  • INVALID_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ài
  • INVALID_VALUE — giá trị nằm ngoài enum/range cho phép
  • INVALID_USER_STATUS — user ở trạng thái không cho phép hành động
  • INVALID_USER_ROLE — role thiếu quyền cho hành động
  • PAYMENT_METHOD_NOT_SUPPORTED — combo provider/asset chưa được enable cho merchant
  • AMOUNT_BELOW_MINIMUM — amount của order dưới sàn theo từng network hoặc theo cấp env. details.floor_usd trong response mang sàn cấu hình (USD) để bạn có thể surface trực tiếp; text message cũ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 gateway
  • TOO_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 DB
  • DATABASE_QUERY_ERROR — query plan fail tại runtime
  • DATABASE_TRANSACTION_ERROR — commit/rollback fail
  • REDIS_CONNECTION_ERROR — không reach được Redis
  • REDIS_OPERATION_ERROR — lệnh Redis fail
  • EXTERNAL_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 fail
  • SENDGRID_SERVICE_ERROR — gửi mail SendGrid fail
  • INTERNAL_SERVER_ERROR — fall-through bất ngờ; capture header Date củ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)

SurfaceGiớ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-LimitX-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.