> ## Documentation Index
> Fetch the complete documentation index at: https://internal.mechzie.in/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Error codes, rate limiting, pagination, and response format reference

## Error Response Format

All API errors follow a standard envelope:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "errors": {
      "field_name": ["validation message"]
    }
  }
}
```

| Field           | Type    | Always present? | Description                                 |
| --------------- | ------- | --------------- | ------------------------------------------- |
| `success`       | boolean | ✅               | Always `false` for errors                   |
| `error.code`    | string  | ✅               | Machine-readable code (use for switch/case) |
| `error.message` | string  | ✅               | Human-readable description                  |
| `error.errors`  | object  | ❌ Only on 422   | Field-level validation errors               |

### Handling in Dart

```dart theme={null}
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

| Code                    | HTTP Status | When                                  | Frontend Action                     |
| ----------------------- | ----------- | ------------------------------------- | ----------------------------------- |
| `UNAUTHORIZED`          | 401         | Missing/invalid/expired JWT           | Refresh token or redirect to login  |
| `FORBIDDEN`             | 403         | Insufficient role permissions         | Show "Access denied"                |
| `ROLE_CONFLICT`         | 403         | User exists with different role       | Show "Already registered as {role}" |
| `NOT_FOUND`             | 404         | Resource doesn't exist                | Show 404 screen                     |
| `CONFLICT`              | 409         | Job already taken by another mechanic | Dismiss offer, show "Already taken" |
| `INVALID_TRANSITION`    | 409         | Invalid job status change             | Refresh job status from API         |
| `VALIDATION_ERROR`      | 422         | Request body fails Zod validation     | Highlight invalid form fields       |
| `RATING_WINDOW_EXPIRED` | 422         | Rating submitted after 7-day window   | Show "Rating period expired"        |
| `PAYMENT_REQUIRED`      | 402         | Unpaid cancellation fees              | Redirect to pay fees                |
| `RATE_LIMIT_EXCEEDED`   | 429         | Too many requests                     | Show "Try again in X seconds"       |
| `INTERNAL_ERROR`        | 500         | Server error                          | Show generic error, retry later     |

## Rate Limiting

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

### Limits

| Scope             | Limit                     | Applied to                    |
| ----------------- | ------------------------- | ----------------------------- |
| **Global**        | 60 requests/minute per IP | All endpoints                 |
| **Auth**          | 10 requests/minute per IP | `/auth/verify-otp` only       |
| **Tracking HTTP** | 1 request/2 seconds       | `GET /tracking/jobs/{id}`     |
| **Admin invite**  | Stricter limit            | `/auth/complete-admin-invite` |

### Socket.io Event Limits

| Event             | Limit     |
| ----------------- | --------- |
| `location:update` | 60/minute |
| `job:join`        | 10/minute |
| `job:leave`       | 10/minute |

### Response Headers

Every response includes rate limit info:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1716300060
```

| Header                  | Description                       |
| ----------------------- | --------------------------------- |
| `X-RateLimit-Limit`     | Max requests in the window        |
| `X-RateLimit-Remaining` | Requests left in current window   |
| `X-RateLimit-Reset`     | Unix timestamp when window resets |

### Handling 429 in Dart

```dart theme={null}
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:

```bash theme={null}
GET /api/v1/jobs?page=2&limit=10&status=completed
```

**Response:**

```json theme={null}
{
  "data": [
    { "id": "...", "status": "completed", ... },
    { "id": "...", "status": "completed", ... }
  ],
  "page": 2,
  "limit": 10,
  "total": 42
}
```

| Field   | Type   | Description                    |
| ------- | ------ | ------------------------------ |
| `data`  | array  | The items for the current page |
| `page`  | number | Current page (1-indexed)       |
| `limit` | number | Items per page (default 20)    |
| `total` | number | Total 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

```dart theme={null}
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:

```json theme={null}
{
  "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:

```dart theme={null}
if (error.code == 'VALIDATION_ERROR' && error.fieldErrors != null) {
  for (final entry in error.fieldErrors!.entries) {
    formKey.currentState?.fields[entry.key]?.invalidate(entry.value.first);
  }
}
```
