Skip to main content

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

Step-by-Step

1

Request a presigned URL

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):
{
  "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
}
2

Upload the file directly to S3

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
}
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.
3

Use the fileUrl in API calls

Pass the returned fileUrl to any endpoint that accepts file URLs:
// 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,
});

Allowed File Types

Content TypeExtensionMax Size
image/jpeg.jpg, .jpeg10 MB
image/png.png10 MB
image/webp.webp10 MB
application/pdf.pdf10 MB
Other content types will be rejected with a 422 VALIDATION_ERROR. Always validate the file type client-side before requesting a presigned URL.

Where File URLs Are Used

FeatureFieldAccepted Types
Job photosphoto_urls array in job creationImages only
Mechanic documentsfile_url in document objectsImages + PDF
User avataravatar_url in profile updateImages only

Mechanic Document Types

When submitting mechanic registration documents, use these type values:
TypeDescription
aadhaarAadhaar card (identity verification)
skill_certSkill or trade certification
photo_idGovernment-issued photo ID
otherAny other supporting document

Dart Helper

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'];
  }
}
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.