Skip to main content

Error Response Format

All API errors follow a standard envelope:
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "errors": {
      "field_name": ["validation message"]
    }
  }
}
FieldTypeAlways present?Description
successbooleanAlways false for errors
error.codestringMachine-readable code (use for switch/case)
error.messagestringHuman-readable description
error.errorsobject❌ Only on 422Field-level validation errors

Handling in Dart

class ApiError {
  final String code;
  final String message;
  final Map<String, List<String>>? fieldErrors;

  factory ApiError.fromJson(Map<String, dynamic> json) {
    final error = json['error'];
    return ApiError(
      code: error['code'],
      message: error['message'],
      fieldErrors: error['errors']?.map(
        (k, v) => MapEntry(k, List<String>.from(v)),
      ),
    );
  }
}

// Usage
if (response.statusCode != 200) {
  final error = ApiError.fromJson(jsonDecode(response.body));
  switch (error.code) {
    case 'VALIDATION_ERROR':
      showFieldErrors(error.fieldErrors!);
      break;
    case 'UNAUTHORIZED':
      await refreshTokenOrLogout();
      break;
    case 'PAYMENT_REQUIRED':
      navigateToPayFees();
      break;
    default:
      showGenericError(error.message);
  }
}

Error Code Reference

CodeHTTP StatusWhenFrontend Action
UNAUTHORIZED401Missing/invalid/expired JWTRefresh token or redirect to login
FORBIDDEN403Insufficient role permissionsShow “Access denied”
ROLE_CONFLICT403User exists with different roleShow “Already registered as
NOT_FOUND404Resource doesn’t existShow 404 screen
CONFLICT409Job already taken by another mechanicDismiss offer, show “Already taken”
INVALID_TRANSITION409Invalid job status changeRefresh job status from API
VALIDATION_ERROR422Request body fails Zod validationHighlight invalid form fields
RATING_WINDOW_EXPIRED422Rating submitted after 7-day windowShow “Rating period expired”
PAYMENT_REQUIRED402Unpaid cancellation feesRedirect to pay fees
RATE_LIMIT_EXCEEDED429Too many requestsShow “Try again in X seconds”
INTERNAL_ERROR500Server errorShow generic error, retry later

Rate Limiting

The API enforces rate limits at multiple levels. When exceeded, you’ll receive a 429 response.

Limits

ScopeLimitApplied to
Global60 requests/minute per IPAll endpoints
Auth10 requests/minute per IP/auth/verify-otp only
Tracking HTTP1 request/2 secondsGET /tracking/jobs/{id}
Admin inviteStricter limit/auth/complete-admin-invite

Socket.io Event Limits

EventLimit
location:update60/minute
job:join10/minute
job:leave10/minute

Response Headers

Every response includes rate limit info:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1716300060
HeaderDescription
X-RateLimit-LimitMax requests in the window
X-RateLimit-RemainingRequests left in current window
X-RateLimit-ResetUnix timestamp when window resets

Handling 429 in Dart

if (response.statusCode == 429) {
  final resetAt = int.parse(response.headers['x-ratelimit-reset'] ?? '0');
  final waitSeconds = resetAt - DateTime.now().millisecondsSinceEpoch ~/ 1000;
  await Future.delayed(Duration(seconds: waitSeconds));
  // Retry the request
}

Pagination

Paginated endpoints accept page and limit query parameters and return a standard envelope:
GET /api/v1/jobs?page=2&limit=10&status=completed
Response:
{
  "data": [
    { "id": "...", "status": "completed", ... },
    { "id": "...", "status": "completed", ... }
  ],
  "page": 2,
  "limit": 10,
  "total": 42
}
FieldTypeDescription
dataarrayThe items for the current page
pagenumberCurrent page (1-indexed)
limitnumberItems per page (default 20)
totalnumberTotal items matching the query

Paginated Endpoints

  • GET /api/v1/jobs — filter by status
  • GET /api/v1/notifications — user notifications
  • GET /api/v1/admin/users — filter by role
  • GET /api/v1/admin/mechanics — mechanic list
  • GET /api/v1/admin/jobs — filter by status
  • GET /api/v1/ratings/mechanic/{id} — mechanic ratings

Dart Helper

class PaginatedResponse<T> {
  final List<T> data;
  final int page;
  final int limit;
  final int total;

  int get totalPages => (total / limit).ceil();
  bool get hasNextPage => page < totalPages;
  bool get hasPreviousPage => page > 1;
}

Validation Errors (422)

When request validation fails (Zod), the errors field contains field-level details:
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "errors": {
      "pickup_lat": ["Number must be between -90 and 90"],
      "pickup_lng": ["Number must be between -180 and 180"],
      "service_category_id": ["Required"]
    }
  }
}
Map these to form field errors in your UI:
if (error.code == 'VALIDATION_ERROR' && error.fieldErrors != null) {
  for (final entry in error.fieldErrors!.entries) {
    formKey.currentState?.fields[entry.key]?.invalidate(entry.value.first);
  }
}