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