# API Error Codes Reference All GoodGo Platform API errors follow a structured JSON format. This document lists every `errorCode` value, its HTTP status, description, and usage examples. ## Error Response Format Every error response from the API has this shape: ```json { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Human-readable error description", "details": { }, "correlationId": "optional-uuid", "timestamp": "2026-04-10T12:00:00.000Z" } ``` | Field | Type | Description | |-----------------|----------|-------------| | `statusCode` | `number` | HTTP status code (e.g. `400`, `401`, `404`). | | `errorCode` | `string` | Machine-readable error identifier (see tables below). | | `message` | `string` | Human-readable explanation (Vietnamese for business rules, English for system errors). | | `details` | `object` | Optional structured metadata (e.g. validation field names, current/target status). | | `correlationId` | `string` | Echoed from the `X-Correlation-Id` request header when present. | | `timestamp` | `string` | ISO 8601 timestamp of the error occurrence. | --- ## General Error Codes These codes apply across all modules. They are also used as fallback codes when a plain `HttpException` (without a domain error code) is thrown. | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `INTERNAL_ERROR` | `500` | Unexpected server error. | Unhandled exceptions, infrastructure failures. | | `VALIDATION_FAILED` | `400` | Request data failed validation. | Invalid input fields, business rule violations (e.g. invalid phone format, bad status transitions). | | `NOT_FOUND` | `404` | Requested resource does not exist. | Entity lookup by ID returns no result. | | `CONFLICT` | `409` | Resource state conflict. | Duplicate creation, conflicting state transition. | | `UNAUTHORIZED` | `401` | Authentication required or failed. | Missing/invalid/expired auth token, bad credentials. | | `FORBIDDEN` | `403` | Insufficient permissions. | Role-based access denial, CSRF token failure, resource ownership mismatch. | | `BAD_REQUEST` | `400` | Malformed request. | Invalid file upload, unsupported content type, bad query parameters. | | `TOO_MANY_REQUESTS` | `429` | Rate limit exceeded. | Too many API requests in a short period. | --- ## Auth Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `AUTH_INVALID_CREDENTIALS` | `401` | Login credentials are incorrect. | Phone number or password mismatch during local login. | | `AUTH_TOKEN_EXPIRED` | `401` | JWT token has expired. | Access token or refresh token past expiration. | | `AUTH_TOKEN_INVALID` | `401` | JWT token is malformed or tampered with. | Token signature verification failure. | | `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Authenticated user lacks required role/permission. | Non-admin accessing admin endpoints, role mismatch. | ### Auth Module Usage Examples ```json // POST /auth/login — wrong password { "statusCode": 401, "errorCode": "UNAUTHORIZED", "message": "Số điện thoại hoặc mật khẩu không đúng" } // POST /auth/refresh — expired refresh token { "statusCode": 401, "errorCode": "UNAUTHORIZED", "message": "Refresh token không hợp lệ hoặc đã hết hạn" } // POST /auth/register — phone already registered { "statusCode": 409, "errorCode": "CONFLICT", "message": "Số điện thoại đã được đăng ký" } // POST /auth/register — invalid phone format { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Số điện thoại không hợp lệ. Yêu cầu 10 chữ số bắt đầu bằng 0" } ``` --- ## User Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `USER_NOT_FOUND` | `404` | User does not exist. | Profile lookup, admin user management. | | `USER_ALREADY_EXISTS` | `409` | User with given identifier already exists. | Duplicate registration (phone or email). | | `USER_INVALID_PHONE` | `400` | Phone number format is invalid. | Registration or profile update with bad phone format. | ### User Module Usage Examples ```json // GET /auth/profile — deleted or nonexistent user { "statusCode": 404, "errorCode": "NOT_FOUND", "message": "Người dùng with id '...' not found" } // DELETE /auth/account — account already deleted { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Tài khoản đã bị xóa" } ``` --- ## Listing Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `LISTING_NOT_FOUND` | `404` | Listing does not exist. | Lookup by ID for viewing, moderation, or status change. | | `LISTING_INVALID_STATUS_TRANSITION` | `400` | Status transition is not allowed. | Attempting an invalid workflow step (e.g. `EXPIRED` → `ACTIVE`). | | `LISTING_ALREADY_ACTIVE` | `409` | Listing is already in active state. | Re-activating a listing that is already published. | | `LISTING_EXPIRED` | `400` | Listing has expired. | Attempting operations on an expired listing. | ### Listing Module Usage Examples ```json // PATCH /listings/:id/status — invalid transition { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Không thể chuyển trạng thái từ EXPIRED sang ACTIVE", "details": { "currentStatus": "EXPIRED", "targetStatus": "ACTIVE" } } // POST /admin/listings/:id/approve — already approved { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Listing này không ở trạng thái chờ duyệt" } // POST /listings — invalid address { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Địa chỉ không hợp lệ" } ``` --- ## Property Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `PROPERTY_NOT_FOUND` | `404` | Property does not exist. | Property lookup for media upload or listing creation. | ### Property Module Usage Examples ```json // POST /properties/:id/media — property not found { "statusCode": 404, "errorCode": "NOT_FOUND", "message": "Property with id '...' not found" } // POST /properties/:id/media — too many files { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Tối đa 30 ảnh/video cho mỗi bất động sản" } ``` --- ## Media Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `MEDIA_UPLOAD_FAILED` | `500` | File upload to storage provider failed. | S3/cloud storage write failure. | | `MEDIA_LIMIT_EXCEEDED` | `400` | Maximum number of media files reached. | Exceeding per-property media limit. | --- ## Payment Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `PAYMENT_FAILED` | `500` | Payment processing failed. | Gateway error, provider timeout, payment link creation failure. | | `PAYMENT_ALREADY_PROCESSED` | `409` | Payment has already been completed, failed, or refunded. | Duplicate callback, retry on non-pending payment, refunding a non-completed payment. | | `PAYMENT_INVALID_AMOUNT` | `400` | Payment amount is invalid. | Zero/negative amount, amount below minimum threshold. | ### Payment Module Usage Examples ```json // POST /payments — duplicate idempotency key { "statusCode": 409, "errorCode": "CONFLICT", "message": "Thanh toán với idempotency key này đã tồn tại" } // POST /payments/callback — invalid signature { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Chữ ký callback không hợp lệ" } // POST /payments/:id/refund — payment not completed { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất" } // Entity-level: marking already-processed payment as completed { "statusCode": 409, "errorCode": "PAYMENT_ALREADY_PROCESSED", "message": "Cannot complete payment in status COMPLETED" } ``` --- ## Subscription Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription does not exist. | Lookup for cancellation, upgrade, or usage metering. | | `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | User already has an active subscription. | Attempting to create a second active subscription. | | `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription has already been cancelled. | Re-cancelling a subscription. | | `SUBSCRIPTION_INACTIVE` | `409` | Subscription is not in an active state. | Upgrading, expiring, or marking past-due on a non-active subscription. | | `QUOTA_EXCEEDED` | `403` | Usage quota for the current plan has been exceeded. | Exceeding listing, lead, or search quota limits. | ### Subscription Module Usage Examples ```json // POST /subscriptions — already has active subscription { "statusCode": 409, "errorCode": "CONFLICT", "message": "Người dùng đã có subscription đang hoạt động" } // POST /subscriptions/cancel — already cancelled { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Subscription đã bị hủy trước đó" } // POST /subscriptions/upgrade — subscription not active { "statusCode": 400, "errorCode": "VALIDATION_FAILED", "message": "Subscription không ở trạng thái hoạt động" } // Entity-level: upgrade on inactive subscription { "statusCode": 409, "errorCode": "SUBSCRIPTION_INACTIVE", "message": "Không thể nâng cấp subscription ở trạng thái CANCELLED" } // Quota guard — plan limit exceeded { "statusCode": 403, "errorCode": "FORBIDDEN", "message": "Bạn đã đạt giới hạn listings. Vui lòng nâng cấp gói để tiếp tục." } ``` --- ## Course Error Codes | Error Code | HTTP Status | Description | When It Occurs | |---|---|---|---| | `COURSE_NOT_FOUND` | `404` | Course does not exist. | Course lookup by ID. | | `COURSE_ALREADY_PUBLISHED` | `409` | Course is already in published state. | Re-publishing an active course. | | `COURSE_ENROLLMENT_CLOSED` | `400` | Course enrollment period has ended. | Attempting to enroll after the cutoff date. | --- ## Error Code Quick-Reference (Sorted Alphabetically) | Error Code | HTTP Status | Module | |---|---|---| | `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Auth | | `AUTH_INVALID_CREDENTIALS` | `401` | Auth | | `AUTH_TOKEN_EXPIRED` | `401` | Auth | | `AUTH_TOKEN_INVALID` | `401` | Auth | | `BAD_REQUEST` | `400` | General | | `CONFLICT` | `409` | General | | `COURSE_ALREADY_PUBLISHED` | `409` | Course | | `COURSE_ENROLLMENT_CLOSED` | `400` | Course | | `COURSE_NOT_FOUND` | `404` | Course | | `FORBIDDEN` | `403` | General | | `INTERNAL_ERROR` | `500` | General | | `LISTING_ALREADY_ACTIVE` | `409` | Listing | | `LISTING_EXPIRED` | `400` | Listing | | `LISTING_INVALID_STATUS_TRANSITION` | `400` | Listing | | `LISTING_NOT_FOUND` | `404` | Listing | | `MEDIA_LIMIT_EXCEEDED` | `400` | Media | | `MEDIA_UPLOAD_FAILED` | `500` | Media | | `NOT_FOUND` | `404` | General | | `PAYMENT_ALREADY_PROCESSED` | `409` | Payment | | `PAYMENT_FAILED` | `500` | Payment | | `PAYMENT_INVALID_AMOUNT` | `400` | Payment | | `PROPERTY_NOT_FOUND` | `404` | Property | | `QUOTA_EXCEEDED` | `403` | Subscription | | `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | Subscription | | `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription | | `SUBSCRIPTION_INACTIVE` | `409` | Subscription | | `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription | | `TOO_MANY_REQUESTS` | `429` | General | | `UNAUTHORIZED` | `401` | General | | `USER_ALREADY_EXISTS` | `409` | User | | `USER_INVALID_PHONE` | `400` | User | | `USER_NOT_FOUND` | `404` | User | --- ## Frontend Integration Guide ### TypeScript Error Type ```typescript interface ApiError { statusCode: number; errorCode: string; message: string; details?: Record; correlationId?: string; timestamp: string; } ``` ### Handling Errors by Code ```typescript import axios, { AxiosError } from 'axios'; async function handleApiCall() { try { const res = await axios.post('/api/listings', payload); return res.data; } catch (err) { if (axios.isAxiosError(err)) { const apiError = err.response?.data as ApiError; switch (apiError.errorCode) { case 'VALIDATION_FAILED': // Show field-level errors from apiError.details showValidationErrors(apiError.details); break; case 'UNAUTHORIZED': case 'AUTH_TOKEN_EXPIRED': // Redirect to login or refresh token await refreshSession(); break; case 'QUOTA_EXCEEDED': // Show upgrade prompt showUpgradeDialog(apiError.message); break; case 'CONFLICT': // Inform user of duplicate action showToast(apiError.message, 'warning'); break; case 'NOT_FOUND': // Navigate to 404 page or show missing resource message router.push('/404'); break; default: showToast('Đã xảy ra lỗi, vui lòng thử lại', 'error'); } } } } ``` ### Error Code Constants (Optional) For type safety, maintain error codes as a union type on the frontend: ```typescript type ErrorCode = | 'INTERNAL_ERROR' | 'VALIDATION_FAILED' | 'NOT_FOUND' | 'CONFLICT' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'BAD_REQUEST' | 'TOO_MANY_REQUESTS' | 'AUTH_INVALID_CREDENTIALS' | 'AUTH_TOKEN_EXPIRED' | 'AUTH_TOKEN_INVALID' | 'AUTH_INSUFFICIENT_PERMISSIONS' | 'USER_NOT_FOUND' | 'USER_ALREADY_EXISTS' | 'USER_INVALID_PHONE' | 'COURSE_NOT_FOUND' | 'COURSE_ALREADY_PUBLISHED' | 'COURSE_ENROLLMENT_CLOSED' | 'LISTING_NOT_FOUND' | 'LISTING_INVALID_STATUS_TRANSITION' | 'LISTING_ALREADY_ACTIVE' | 'LISTING_EXPIRED' | 'PROPERTY_NOT_FOUND' | 'MEDIA_UPLOAD_FAILED' | 'MEDIA_LIMIT_EXCEEDED' | 'PAYMENT_FAILED' | 'PAYMENT_ALREADY_PROCESSED' | 'PAYMENT_INVALID_AMOUNT' | 'SUBSCRIPTION_NOT_FOUND' | 'SUBSCRIPTION_ALREADY_ACTIVE' | 'SUBSCRIPTION_ALREADY_CANCELLED' | 'SUBSCRIPTION_INACTIVE' | 'QUOTA_EXCEEDED'; ``` --- ## Source Files - **Error code enum**: `apps/api/src/modules/shared/domain/error-codes.ts` - **Domain exceptions**: `apps/api/src/modules/shared/domain/domain-exception.ts` - **Global exception filter**: `apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts`