> ## 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.

# File Uploads

> S3 presigned URL flow for uploading images and documents

## Overview

MechZie uses **S3-compatible presigned URLs** for file uploads. Files are uploaded directly from the client to S3 — they never pass through the API server. This keeps uploads fast and reduces server load.

## Upload Flow

```mermaid theme={null}
sequenceDiagram
    participant App as Flutter App
    participant API as MechZie API
    participant S3 as S3 Storage

    App->>API: POST /uploads/presigned-url
    API-->>App: { uploadUrl, fileUrl, expiresIn }
    App->>S3: PUT file to uploadUrl
    S3-->>App: 200 OK
    App->>API: Use fileUrl in subsequent requests
```

## Step-by-Step

<Steps>
  <Step title="Request a presigned URL">
    ```bash theme={null}
    curl -X POST https://mechzie-api-production.run.app/api/v1/uploads/presigned-url \
      -H "Authorization: Bearer ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "filename": "tire-damage.jpg",
        "contentType": "image/jpeg"
      }'
    ```

    **Response (200):**

    ```json theme={null}
    {
      "uploadUrl": "https://s3.ap-south-1.amazonaws.com/mechzie-uploads/...",
      "fileUrl": "https://s3.ap-south-1.amazonaws.com/mechzie-uploads/uuid/tire-damage.jpg",
      "expiresIn": 900
    }
    ```
  </Step>

  <Step title="Upload the file directly to S3">
    ```dart theme={null}
    final bytes = await File(pickedFile.path).readAsBytes();

    final response = await http.put(
      Uri.parse(uploadUrl),
      headers: {'Content-Type': 'image/jpeg'},
      body: bytes,
    );

    if (response.statusCode == 200) {
      // Upload successful — use fileUrl
    }
    ```

    <Warning>
      The `Content-Type` header in the PUT request **must match** the `contentType` you sent when requesting the presigned URL. Mismatches will cause a 403 from S3.
    </Warning>
  </Step>

  <Step title="Use the fileUrl in API calls">
    Pass the returned `fileUrl` to any endpoint that accepts file URLs:

    ```dart theme={null}
    // Job creation with photos
    await api.post('/jobs', body: {
      'service_category_id': categoryId,
      'pickup_address': address,
      'pickup_lat': lat,
      'pickup_lng': lng,
      'photo_urls': [fileUrl],  // Use the fileUrl from step 1
    });

    // Mechanic document upload
    await api.post('/mechanics/register', body: {
      'service_category_ids': [categoryId],
      'service_radius_km': 10,
      'documents': [
        {
          'type': 'aadhaar',
          'file_url': fileUrl,  // Use the fileUrl from step 1
        }
      ],
    });

    // Avatar update
    await api.patch('/users/me', body: {
      'avatar_url': fileUrl,
    });
    ```
  </Step>
</Steps>

## Allowed File Types

| Content Type      | Extension   | Max Size |
| ----------------- | ----------- | -------- |
| `image/jpeg`      | .jpg, .jpeg | 10 MB    |
| `image/png`       | .png        | 10 MB    |
| `image/webp`      | .webp       | 10 MB    |
| `application/pdf` | .pdf        | 10 MB    |

<Warning>
  Other content types will be rejected with a **422 VALIDATION\_ERROR**. Always validate the file type client-side before requesting a presigned URL.
</Warning>

## Where File URLs Are Used

| Feature            | Field                              | Accepted Types |
| ------------------ | ---------------------------------- | -------------- |
| Job photos         | `photo_urls` array in job creation | Images only    |
| Mechanic documents | `file_url` in document objects     | Images + PDF   |
| User avatar        | `avatar_url` in profile update     | Images only    |

## Mechanic Document Types

When submitting mechanic registration documents, use these type values:

| Type         | Description                          |
| ------------ | ------------------------------------ |
| `aadhaar`    | Aadhaar card (identity verification) |
| `skill_cert` | Skill or trade certification         |
| `photo_id`   | Government-issued photo ID           |
| `other`      | Any other supporting document        |

## Dart Helper

```dart theme={null}
class UploadService {
  final ApiClient api;

  Future<String> uploadFile(File file, String contentType) async {
    // 1. Get presigned URL
    final presigned = await api.post('/uploads/presigned-url', body: {
      'filename': path.basename(file.path),
      'contentType': contentType,
    });

    // 2. Upload to S3
    final bytes = await file.readAsBytes();
    final uploadResponse = await http.put(
      Uri.parse(presigned['uploadUrl']),
      headers: {'Content-Type': contentType},
      body: bytes,
    );

    if (uploadResponse.statusCode != 200) {
      throw Exception('Upload failed: ${uploadResponse.statusCode}');
    }

    // 3. Return the permanent URL
    return presigned['fileUrl'];
  }
}
```

<Note>
  Presigned URLs expire in **15 minutes** (900 seconds). If the upload is not completed within this time, request a new URL. The `fileUrl` remains valid permanently after a successful upload.
</Note>
